Add device-specific Certificate Providers

Reviewed By: mweststrate

Differential Revision: D33821880

fbshipit-source-id: c75c71db4d7dc680f75cf41ba2d5dad009a5fd03
This commit is contained in:
Andrey Goncharov
2022-02-02 03:05:34 -08:00
committed by Facebook GitHub Bot
parent b9aeaa9339
commit 29f6d0e711
7 changed files with 414 additions and 334 deletions

View File

@@ -45,6 +45,7 @@ import {
extractAppNameFromCSR, extractAppNameFromCSR,
loadSecureServerConfig, loadSecureServerConfig,
} from '../utils/certificateUtils'; } from '../utils/certificateUtils';
import DesktopCertificateProvider from '../devices/desktop/DesktopCertificateProvider';
type ClientTimestampTracker = { type ClientTimestampTracker = {
insecureStart?: number; insecureStart?: number;
@@ -83,7 +84,6 @@ class ServerController extends EventEmitter implements ServerEventsListener {
altInsecureServer: ServerAdapter | null = null; altInsecureServer: ServerAdapter | null = null;
browserServer: ServerAdapter | null = null; browserServer: ServerAdapter | null = null;
certificateProvider: CertificateProvider;
connectionTracker: ConnectionTracker; connectionTracker: ConnectionTracker;
flipperServer: FlipperServerImpl; flipperServer: FlipperServerImpl;
@@ -93,7 +93,6 @@ class ServerController extends EventEmitter implements ServerEventsListener {
constructor(flipperServer: FlipperServerImpl) { constructor(flipperServer: FlipperServerImpl) {
super(); super();
this.flipperServer = flipperServer; this.flipperServer = flipperServer;
this.certificateProvider = new CertificateProvider(this);
this.connectionTracker = new ConnectionTracker(this.logger); this.connectionTracker = new ConnectionTracker(this.logger);
} }
@@ -278,9 +277,35 @@ class ServerController extends EventEmitter implements ServerEventsListener {
appDirectory: string, appDirectory: string,
medium: CertificateExchangeMedium, medium: CertificateExchangeMedium,
): Promise<{deviceId: string}> { ): Promise<{deviceId: string}> {
let certificateProvider: CertificateProvider;
switch (clientQuery.os) {
case 'Android': {
certificateProvider = this.flipperServer.android.certificateProvider;
break;
}
case 'iOS': {
certificateProvider = this.flipperServer.ios.certificateProvider;
break;
}
// Used by Spark AR studio (search for SkylightFlipperClient)
// See D30992087
case 'MacOS':
case 'Windows': {
certificateProvider = new DesktopCertificateProvider(
this.flipperServer.keytarManager,
);
break;
}
default: {
throw new Error(
`ServerController.onProcessCSR -> os ${clientQuery.os} does not support certificate exchange.`,
);
}
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
reportPlatformFailures( reportPlatformFailures(
this.certificateProvider.processCertificateSigningRequest( certificateProvider.processCertificateSigningRequest(
unsanitizedCSR, unsanitizedCSR,
clientQuery.os, clientQuery.os,
appDirectory, appDirectory,
@@ -350,8 +375,7 @@ class ServerController extends EventEmitter implements ServerEventsListener {
const app_name = await extractAppNameFromCSR(csr); const app_name = await extractAppNameFromCSR(csr);
// TODO: allocate new object, kept now as is to keep changes minimal // TODO: allocate new object, kept now as is to keep changes minimal
(query as any).device_id = (query as any).device_id =
await this.certificateProvider.getTargetDeviceId( await this.flipperServer.android.certificateProvider.getTargetDeviceId(
query.os,
app_name, app_name,
csr_path, csr_path,
csr, csr,

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import CertificateProvider from '../../utils/CertificateProvider';
import {Client} from 'adbkit';
import {KeytarManager} from '../../utils/keytar';
import * as androidUtil from './androidContainerUtility';
import {csrFileName} from '../../utils/certificateUtils';
const logTag = 'AndroidCertificateProvider';
export default class AndroidCertificateProvider extends CertificateProvider {
constructor(keytarManager: KeytarManager, private adb: Client) {
super(keytarManager);
}
async getTargetDeviceId(
appName: string,
appDirectory: string,
csr: string,
): Promise<string> {
const devicesInAdb = await this.adb.listDevices();
if (devicesInAdb.length === 0) {
throw new Error('No Android devices found');
}
const deviceMatchList = devicesInAdb.map(async (device) => {
try {
const result = await this.androidDeviceHasMatchingCSR(
appDirectory,
device.id,
appName,
csr,
);
return {id: device.id, ...result, error: null};
} catch (e) {
console.warn(
`Unable to check for matching CSR in ${device.id}:${appName}`,
logTag,
e,
);
return {id: device.id, isMatch: false, foundCsr: null, error: e};
}
});
const devices = await Promise.all(deviceMatchList);
const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id);
if (matchingIds.length == 0) {
const erroredDevice = devices.find((d) => d.error);
if (erroredDevice) {
throw erroredDevice.error;
}
const foundCsrs = devices
.filter((d) => d.foundCsr !== null)
.map((d) => (d.foundCsr ? encodeURI(d.foundCsr) : 'null'));
console.warn(`Looking for CSR (url encoded):
${encodeURI(this.santitizeString(csr))}
Found these:
${foundCsrs.join('\n\n')}`);
throw new Error(`No matching device found for app: ${appName}`);
}
if (matchingIds.length > 1) {
console.warn(
new Error('[conn] More than one matching device found for CSR'),
csr,
);
}
return matchingIds[0];
}
protected async handleFSBasedDeploy(
destination: string,
filename: string,
contents: string,
csr: string,
appName: string,
) {
const deviceId = await this.getTargetDeviceId(appName, destination, csr);
await androidUtil.push(
this.adb,
deviceId,
appName,
destination + filename,
contents,
);
}
private async androidDeviceHasMatchingCSR(
directory: string,
deviceId: string,
processName: string,
csr: string,
): Promise<{isMatch: boolean; foundCsr: string}> {
const deviceCsr = await androidUtil.pull(
this.adb,
deviceId,
processName,
directory + csrFileName,
);
// Santitize both of the string before comparation
// The csr string extraction on client side return string in both way
const [sanitizedDeviceCsr, sanitizedClientCsr] = [
deviceCsr.toString(),
csr,
].map((s) => this.santitizeString(s));
const isMatch = sanitizedDeviceCsr === sanitizedClientCsr;
return {isMatch: isMatch, foundCsr: sanitizedDeviceCsr};
}
}

View File

@@ -19,10 +19,24 @@ import {
getServerPortsConfig, getServerPortsConfig,
getFlipperServerConfig, getFlipperServerConfig,
} from '../../FlipperServerConfig'; } from '../../FlipperServerConfig';
import AndroidCertificateProvider from './AndroidCertificateProvider';
import {assertNotNull} from '../../comms/Utilities';
export class AndroidDeviceManager { export class AndroidDeviceManager {
private adbClient?: ADBClient;
constructor(public flipperServer: FlipperServerImpl) {} constructor(public flipperServer: FlipperServerImpl) {}
public get certificateProvider() {
assertNotNull(
this.adbClient,
'AndroidDeviceManager.certificateProvider -> missing adbClient',
);
return new AndroidCertificateProvider(
this.flipperServer.keytarManager,
this.adbClient,
);
}
private createDevice( private createDevice(
adbClient: ADBClient, adbClient: ADBClient,
device: Device, device: Device,
@@ -177,6 +191,8 @@ export class AndroidDeviceManager {
); );
} }
this.adbClient = client;
client client
.trackDevices() .trackDevices()
.then((tracker) => { .then((tracker) => {

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import CertificateProvider from '../../utils/CertificateProvider';
import fs from 'fs-extra';
export default class DesktopCertificateProvider extends CertificateProvider {
async getTargetDeviceId(): Promise<string> {
// TODO: Could we use some real device serial? Currently, '' corresponds to a local device.
// Whats if some app connects from a remote device?
// What if two apps connect from several different remote devices?
return '';
}
protected async handleFSBasedDeploy(
destination: string,
filename: string,
contents: string,
) {
await fs.writeFile(destination + filename, contents);
}
}

View File

@@ -0,0 +1,189 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {IdbConfig} from './idbConfig';
import {KeytarManager} from '../../utils/keytar';
import CertificateProvider from '../../utils/CertificateProvider';
import iosUtil from './iOSContainerUtility';
import fs from 'fs-extra';
import {promisify} from 'util';
import tmp, {DirOptions} from 'tmp';
import {csrFileName} from '../../utils/certificateUtils';
import path from 'path';
const tmpDir = promisify(tmp.dir) as (options?: DirOptions) => Promise<string>;
// eslint-disable-next-line @typescript-eslint/naming-convention
export default class iOSCertificateProvider extends CertificateProvider {
constructor(keytarManager: KeytarManager, private idbConfig: IdbConfig) {
super(keytarManager);
}
async getTargetDeviceId(
appName: string,
appDirectory: string,
csr: string,
): Promise<string> {
const matches = /\/Devices\/([^/]+)\//.exec(appName);
if (matches && matches.length == 2) {
// It's a simulator, the deviceId is in the filepath.
return matches[1];
}
const targets = await iosUtil.targets(
this.idbConfig.idbPath,
this.idbConfig.enablePhysicalIOS,
);
if (targets.length === 0) {
throw new Error('No iOS devices found');
}
const deviceMatchList = targets.map(async (target) => {
const isMatch = await this.iOSDeviceHasMatchingCSR(
appDirectory,
target.udid,
appName,
csr,
);
return {id: target.udid, isMatch};
});
const devices = await Promise.all(deviceMatchList);
const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id);
if (matchingIds.length == 0) {
throw new Error(`No matching device found for app: ${appName}`);
}
if (matchingIds.length > 1) {
console.warn(`Multiple devices found for app: ${appName}`);
}
return matchingIds[0];
}
protected async handleFSBasedDeploy(
destination: string,
filename: string,
contents: string,
csr: string,
appName: string,
) {
try {
await fs.writeFile(destination + filename, contents);
} catch (err) {
// Writing directly to FS failed. It's probably a physical device.
const relativePathInsideApp =
this.getRelativePathInAppContainer(destination);
const udid = await this.getTargetDeviceId(appName, destination, csr);
await this.pushFileToiOSDevice(
udid,
appName,
relativePathInsideApp,
filename,
contents,
);
}
}
private getRelativePathInAppContainer(absolutePath: string) {
const matches = /Application\/[^/]+\/(.*)/.exec(absolutePath);
if (matches && matches.length === 2) {
return matches[1];
}
throw new Error("Path didn't match expected pattern: " + absolutePath);
}
private async pushFileToiOSDevice(
udid: string,
bundleId: string,
destination: string,
filename: string,
contents: string,
): Promise<void> {
const dir = await tmpDir({unsafeCleanup: true});
const filePath = path.resolve(dir, filename);
await fs.writeFile(filePath, contents);
await iosUtil.push(
udid,
filePath,
bundleId,
destination,
this.idbConfig.idbPath,
);
}
private async iOSDeviceHasMatchingCSR(
directory: string,
deviceId: string,
bundleId: string,
csr: string,
): Promise<boolean> {
const originalFile = this.getRelativePathInAppContainer(
path.resolve(directory, csrFileName),
);
const dir = await tmpDir({unsafeCleanup: true});
// Workaround for idb weirdness
// Originally started at D27590885
// Re-appared at https://github.com/facebook/flipper/issues/3009
//
// People reported various workarounds. None of them worked consistently for everyone.
// Usually, the workarounds included re-building idb from source or re-installing it.
//
// The only more or less reasonable explanation I was able to find is that the final behavior depends on whether the idb_companion is local or not.
//
// This is how idb_companion sets its locality
// https://github.com/facebook/idb/blob/main/idb_companion/Server/FBIDBServiceHandler.mm#L1507
// idb sends a connection request and provides a file path to a temporary file. idb_companion checks if it can access that file.
//
// So when it is "local", the pulled filed is written directly to the destination path
// https://github.com/facebook/idb/blob/main/idb/grpc/client.py#L698
// So it is expected that the destination path ends with a file to write to.
// However, if the companion is remote, then we seem to get here https://github.com/facebook/idb/blob/71791652efa2d5e6f692cb8985ff0d26b69bf08f/idb/common/tar.py#L232
// Where we create a tree of directories and write the file stream there.
//
// So the only explanation I could come up with is that somehow, by re-installing idb and playing with the env, people could affect the locality of the idb_companion.
//
// The ultimate workaround is to try pulling the cert file without the cert name attached first, if it fails, try to append it.
try {
await iosUtil.pull(
deviceId,
originalFile,
bundleId,
dir,
this.idbConfig.idbPath,
);
} catch (e) {
console.warn(
'Original idb pull failed. Most likely it is a physical device that requires us to handle the dest path dirrently. Forcing a re-try with the updated dest path. See D32106952 for details. Original error:',
e,
);
await iosUtil.pull(
deviceId,
originalFile,
bundleId,
path.join(dir, csrFileName),
this.idbConfig.idbPath,
);
console.info(
'Subsequent idb pull succeeded. Nevermind previous wranings.',
);
}
const items = await fs.readdir(dir);
if (items.length > 1) {
throw new Error('Conflict in temp dir');
}
if (items.length === 0) {
throw new Error('Failed to pull CSR from device');
}
const fileName = items[0];
const copiedFile = path.resolve(dir, fileName);
console.debug('Trying to read CSR from', copiedFile);
const data = await fs.readFile(copiedFile);
const csrFromDevice = this.santitizeString(data.toString());
return csrFromDevice === this.santitizeString(csr);
}
}

View File

@@ -23,6 +23,8 @@ import {
import {FlipperServerImpl} from '../../FlipperServerImpl'; import {FlipperServerImpl} from '../../FlipperServerImpl';
import {getFlipperServerConfig} from '../../FlipperServerConfig'; import {getFlipperServerConfig} from '../../FlipperServerConfig';
import {IdbConfig, setIdbConfig} from './idbConfig'; import {IdbConfig, setIdbConfig} from './idbConfig';
import {assertNotNull} from '../../comms/Utilities';
import iOSCertificateProvider from './iOSCertificateProvider';
export class IOSDeviceManager { export class IOSDeviceManager {
private portForwarders: Array<ChildProcess> = []; private portForwarders: Array<ChildProcess> = [];
@@ -39,6 +41,17 @@ export class IOSDeviceManager {
constructor(private flipperServer: FlipperServerImpl) {} constructor(private flipperServer: FlipperServerImpl) {}
public get certificateProvider() {
assertNotNull(
this.idbConfig,
'IOSDeviceManager.certificateProvider -> missing idbConfig',
);
return new iOSCertificateProvider(
this.flipperServer.keytarManager,
this.idbConfig,
);
}
private forwardPort(port: number, multiplexChannelPort: number) { private forwardPort(port: number, multiplexChannelPort: number) {
const child = childProcess.execFile( const child = childProcess.execFile(
this.portforwardingClient, this.portforwardingClient,

View File

@@ -7,24 +7,15 @@
* @format * @format
*/ */
import ServerController from '../comms/ServerController';
import {promisify} from 'util'; import {promisify} from 'util';
import fs from 'fs-extra'; import fs from 'fs-extra';
import tmp from 'tmp';
import path from 'path';
import tmp, {DirOptions} from 'tmp';
import iosUtil from '../devices/ios/iOSContainerUtility';
import {reportPlatformFailures} from 'flipper-common'; import {reportPlatformFailures} from 'flipper-common';
import {getAdbClient} from '../devices/android/adbClient';
import * as androidUtil from '../devices/android/androidContainerUtility';
import archiver from 'archiver'; import archiver from 'archiver';
import {timeout} from 'flipper-common'; import {timeout} from 'flipper-common';
import {v4 as uuid} from 'uuid'; import {v4 as uuid} from 'uuid';
import {internGraphPOSTAPIRequest} from '../fb-stubs/internRequests'; import {internGraphPOSTAPIRequest} from '../fb-stubs/internRequests';
import {getIdbConfig} from '../devices/ios/idbConfig';
import {assertNotNull} from '../comms/Utilities';
import { import {
csrFileName,
deviceCAcertFile, deviceCAcertFile,
deviceClientCertFile, deviceClientCertFile,
ensureOpenSSLIsAvailable, ensureOpenSSLIsAvailable,
@@ -32,39 +23,12 @@ import {
generateClientCertificate, generateClientCertificate,
getCACertificate, getCACertificate,
} from './certificateUtils'; } from './certificateUtils';
import {SERVICE_FLIPPER} from './keytar'; import {KeytarManager, SERVICE_FLIPPER} from './keytar';
export type CertificateExchangeMedium = 'FS_ACCESS' | 'WWW' | 'NONE'; export type CertificateExchangeMedium = 'FS_ACCESS' | 'WWW' | 'NONE';
const tmpDir = promisify(tmp.dir) as (options?: DirOptions) => Promise<string>; export default abstract class CertificateProvider {
constructor(private readonly keytarManager: KeytarManager) {}
const logTag = 'CertificateProvider';
/*
* This class is responsible for generating and deploying server and client
* certificates to allow for secure communication between Flipper and apps.
* It takes a Certificate Signing Request which was generated by the app,
* using the app's public/private keypair.
* With this CSR it uses the Flipper CA to sign a client certificate which it
* deploys securely to the app.
* It also deploys the Flipper CA cert to the app.
* The app can trust a server if and only if it has a certificate signed by the
* Flipper CA.
*/
export default class CertificateProvider {
private server: ServerController;
constructor(server: ServerController) {
this.server = server;
}
private get adb() {
return getAdbClient();
}
private get idbConfig() {
return getIdbConfig();
}
private uploadFiles = async ( private uploadFiles = async (
zipPath: string, zipPath: string,
@@ -85,9 +49,7 @@ export default class CertificateProvider {
}, },
}, },
{timeout: 5 * 60 * 1000}, {timeout: 5 * 60 * 1000},
await this.server.flipperServer.keytarManager.retrieveToken( await this.keytarManager.retrieveToken(SERVICE_FLIPPER),
SERVICE_FLIPPER,
),
).then(() => {}), ).then(() => {}),
'Timed out uploading Flipper certificates to WWW.', 'Timed out uploading Flipper certificates to WWW.',
), ),
@@ -110,29 +72,27 @@ export default class CertificateProvider {
const certFolder = rootFolder + '/FlipperCerts/'; const certFolder = rootFolder + '/FlipperCerts/';
const certsZipPath = rootFolder + '/certs.zip'; const certsZipPath = rootFolder + '/certs.zip';
const caCert = await getCACertificate(); const caCert = await getCACertificate();
await this.deployOrStageFileForMobileApp( await this.deployOrStageFileForDevice(
appDirectory, appDirectory,
deviceCAcertFile, deviceCAcertFile,
caCert, caCert,
csr, csr,
os,
medium, medium,
certFolder, certFolder,
); );
const clientCert = await generateClientCertificate(csr); const clientCert = await generateClientCertificate(csr);
await this.deployOrStageFileForMobileApp( await this.deployOrStageFileForDevice(
appDirectory, appDirectory,
deviceClientCertFile, deviceClientCertFile,
clientCert, clientCert,
csr, csr,
os,
medium, medium,
certFolder, certFolder,
); );
const appName = await extractAppNameFromCSR(csr); const appName = await extractAppNameFromCSR(csr);
const deviceId = const deviceId =
medium === 'FS_ACCESS' medium === 'FS_ACCESS'
? await this.getTargetDeviceId(os, appName, appDirectory, csr) ? await this.getTargetDeviceId(appName, appDirectory, csr)
: uuid(); : uuid();
if (medium === 'WWW') { if (medium === 'WWW') {
const zipPromise = new Promise((resolve, reject) => { const zipPromise = new Promise((resolve, reject) => {
@@ -164,36 +124,17 @@ export default class CertificateProvider {
}; };
} }
getTargetDeviceId( abstract getTargetDeviceId(
os: string, _appName: string,
appName: string, _appDirectory: string,
appDirectory: string, _csr: string,
csr: string, ): Promise<string>;
): Promise<string> {
if (os === 'Android') {
return this.getTargetAndroidDeviceId(appName, appDirectory, csr);
} else if (os === 'iOS') {
return this.getTargetiOSDeviceId(appName, appDirectory, csr);
} else if (os == 'MacOS') {
return Promise.resolve('');
}
return Promise.resolve('unknown');
}
private getRelativePathInAppContainer(absolutePath: string) { private async deployOrStageFileForDevice(
const matches = /Application\/[^/]+\/(.*)/.exec(absolutePath);
if (matches && matches.length === 2) {
return matches[1];
}
throw new Error("Path didn't match expected pattern: " + absolutePath);
}
private async deployOrStageFileForMobileApp(
destination: string, destination: string,
filename: string, filename: string,
contents: string, contents: string,
csr: string, csr: string,
os: string,
medium: CertificateExchangeMedium, medium: CertificateExchangeMedium,
certFolder: string, certFolder: string,
): Promise<void> { ): Promise<void> {
@@ -213,265 +154,18 @@ export default class CertificateProvider {
} }
const appName = await extractAppNameFromCSR(csr); const appName = await extractAppNameFromCSR(csr);
this.handleFSBasedDeploy(destination, filename, contents, csr, appName);
if (os === 'Android') {
assertNotNull(this.adb);
const deviceId = await this.getTargetAndroidDeviceId(
appName,
destination,
csr,
);
await androidUtil.push(
this.adb,
deviceId,
appName,
destination + filename,
contents,
);
} else if (
os === 'iOS' ||
os === 'windows' ||
os == 'MacOS' /* Used by Spark AR?! */
) {
try {
await fs.writeFile(destination + filename, contents);
} catch (err) {
// Writing directly to FS failed. It's probably a physical device.
const relativePathInsideApp =
this.getRelativePathInAppContainer(destination);
const udid = await this.getTargetiOSDeviceId(appName, destination, csr);
await this.pushFileToiOSDevice(
udid,
appName,
relativePathInsideApp,
filename,
contents,
);
}
} else {
throw new Error(`Unsupported device OS for Certificate Exchange: ${os}`);
}
} }
private async pushFileToiOSDevice( protected abstract handleFSBasedDeploy(
udid: string, _destination: string,
bundleId: string, _filename: string,
destination: string, _contents: string,
filename: string, _csr: string,
contents: string, _appName: string,
): Promise<void> { ): Promise<void>;
assertNotNull(this.idbConfig);
const dir = await tmpDir({unsafeCleanup: true}); protected santitizeString(csrString: string): string {
const filePath = path.resolve(dir, filename);
await fs.writeFile(filePath, contents);
await iosUtil.push(
udid,
filePath,
bundleId,
destination,
this.idbConfig.idbPath,
);
}
private async getTargetAndroidDeviceId(
appName: string,
deviceCsrFilePath: string,
csr: string,
): Promise<string> {
assertNotNull(this.adb);
const devicesInAdb = await this.adb.listDevices();
if (devicesInAdb.length === 0) {
throw new Error('No Android devices found');
}
const deviceMatchList = devicesInAdb.map(async (device) => {
try {
const result = await this.androidDeviceHasMatchingCSR(
deviceCsrFilePath,
device.id,
appName,
csr,
);
return {id: device.id, ...result, error: null};
} catch (e) {
console.warn(
`Unable to check for matching CSR in ${device.id}:${appName}`,
logTag,
e,
);
return {id: device.id, isMatch: false, foundCsr: null, error: e};
}
});
const devices = await Promise.all(deviceMatchList);
const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id);
if (matchingIds.length == 0) {
const erroredDevice = devices.find((d) => d.error);
if (erroredDevice) {
throw erroredDevice.error;
}
const foundCsrs = devices
.filter((d) => d.foundCsr !== null)
.map((d) => (d.foundCsr ? encodeURI(d.foundCsr) : 'null'));
console.warn(`Looking for CSR (url encoded):
${encodeURI(this.santitizeString(csr))}
Found these:
${foundCsrs.join('\n\n')}`);
throw new Error(`No matching device found for app: ${appName}`);
}
if (matchingIds.length > 1) {
console.warn(
new Error('[conn] More than one matching device found for CSR'),
csr,
);
}
return matchingIds[0];
}
private async getTargetiOSDeviceId(
appName: string,
deviceCsrFilePath: string,
csr: string,
): Promise<string> {
assertNotNull(this.idbConfig);
const matches = /\/Devices\/([^/]+)\//.exec(deviceCsrFilePath);
if (matches && matches.length == 2) {
// It's a simulator, the deviceId is in the filepath.
return matches[1];
}
const targets = await iosUtil.targets(
this.idbConfig.idbPath,
this.idbConfig.enablePhysicalIOS,
);
if (targets.length === 0) {
throw new Error('No iOS devices found');
}
const deviceMatchList = targets.map(async (target) => {
const isMatch = await this.iOSDeviceHasMatchingCSR(
deviceCsrFilePath,
target.udid,
appName,
csr,
);
return {id: target.udid, isMatch};
});
const devices = await Promise.all(deviceMatchList);
const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id);
if (matchingIds.length == 0) {
throw new Error(`No matching device found for app: ${appName}`);
}
if (matchingIds.length > 1) {
console.warn(`Multiple devices found for app: ${appName}`);
}
return matchingIds[0];
}
private async androidDeviceHasMatchingCSR(
directory: string,
deviceId: string,
processName: string,
csr: string,
): Promise<{isMatch: boolean; foundCsr: string}> {
assertNotNull(this.adb);
const deviceCsr = await androidUtil.pull(
this.adb,
deviceId,
processName,
directory + csrFileName,
);
// Santitize both of the string before comparation
// The csr string extraction on client side return string in both way
const [sanitizedDeviceCsr, sanitizedClientCsr] = [
deviceCsr.toString(),
csr,
].map((s) => this.santitizeString(s));
const isMatch = sanitizedDeviceCsr === sanitizedClientCsr;
return {isMatch: isMatch, foundCsr: sanitizedDeviceCsr};
}
private async iOSDeviceHasMatchingCSR(
directory: string,
deviceId: string,
bundleId: string,
csr: string,
): Promise<boolean> {
assertNotNull(this.idbConfig);
const originalFile = this.getRelativePathInAppContainer(
path.resolve(directory, csrFileName),
);
const dir = await tmpDir({unsafeCleanup: true});
// Workaround for idb weirdness
// Originally started at D27590885
// Re-appared at https://github.com/facebook/flipper/issues/3009
//
// People reported various workarounds. None of them worked consistently for everyone.
// Usually, the workarounds included re-building idb from source or re-installing it.
//
// The only more or less reasonable explanation I was able to find is that the final behavior depends on whether the idb_companion is local or not.
//
// This is how idb_companion sets its locality
// https://github.com/facebook/idb/blob/main/idb_companion/Server/FBIDBServiceHandler.mm#L1507
// idb sends a connection request and provides a file path to a temporary file. idb_companion checks if it can access that file.
//
// So when it is "local", the pulled filed is written directly to the destination path
// https://github.com/facebook/idb/blob/main/idb/grpc/client.py#L698
// So it is expected that the destination path ends with a file to write to.
// However, if the companion is remote, then we seem to get here https://github.com/facebook/idb/blob/71791652efa2d5e6f692cb8985ff0d26b69bf08f/idb/common/tar.py#L232
// Where we create a tree of directories and write the file stream there.
//
// So the only explanation I could come up with is that somehow, by re-installing idb and playing with the env, people could affect the locality of the idb_companion.
//
// The ultimate workaround is to try pulling the cert file without the cert name attached first, if it fails, try to append it.
try {
await iosUtil.pull(
deviceId,
originalFile,
bundleId,
dir,
this.idbConfig.idbPath,
);
} catch (e) {
console.warn(
'Original idb pull failed. Most likely it is a physical device that requires us to handle the dest path dirrently. Forcing a re-try with the updated dest path. See D32106952 for details. Original error:',
e,
);
await iosUtil.pull(
deviceId,
originalFile,
bundleId,
path.join(dir, csrFileName),
this.idbConfig.idbPath,
);
console.info(
'Subsequent idb pull succeeded. Nevermind previous wranings.',
);
}
const items = await fs.readdir(dir);
if (items.length > 1) {
throw new Error('Conflict in temp dir');
}
if (items.length === 0) {
throw new Error('Failed to pull CSR from device');
}
const fileName = items[0];
const copiedFile = path.resolve(dir, fileName);
console.debug('Trying to read CSR from', copiedFile);
const data = await fs.readFile(copiedFile);
const csrFromDevice = this.santitizeString(data.toString());
return csrFromDevice === this.santitizeString(csr);
}
private santitizeString(csrString: string): string {
return csrString.replace(/\r/g, '').trim(); return csrString.replace(/\r/g, '').trim();
} }
} }