text: /**
   * Copyright 2018-present Facebook.
   * This source code is licensed under the MIT license found in the
   * LICENSE file in the root directory of this source tree.
   * @format
   */
  
  import LogManager from '../fb-stubs/Logger';
  const fs = require('fs');
  const adb = require('adbkit-fb');
  import {
    openssl,
    isInstalled as opensslInstalled,
  } from './openssl-wrapper-with-promises';
  const path = require('path');
  
  // Desktop file paths
  const os = require('os');
  const caKey = getFilePath('ca.key');
  const caCert = getFilePath('ca.crt');
  const serverKey = getFilePath('server.key');
  const serverCsr = getFilePath('server.csr');
  const serverCert = getFilePath('server.crt');
  
  // Device file paths
  const csrFileName = 'app.csr';
  const deviceCAcertFile = 'sonarCA.crt';
  const deviceClientCertFile = 'device.crt';
  
  const caSubject = '/C=US/ST=CA/L=Menlo Park/O=Sonar/CN=SonarCA';
  const serverSubject = '/C=US/ST=CA/L=Menlo Park/O=Sonar/CN=localhost';
  const minCertExpiryWindowSeconds = 24 * 60 * 60;
  const appNotDebuggableRegex = /debuggable/;
  const allowedAppNameRegex = /^[a-zA-Z0-9.\-]+$/;
  const allowedAppDirectoryRegex = /^\/[ a-zA-Z0-9.\-\/]+$/;
  
  export type SecureServerConfig = {|
    key: Buffer,
    cert: Buffer,
    ca: Buffer,
    requestCert: boolean,
    rejectUnauthorized: boolean,
  |};
  
  /*
   * This class is responsible for generating and deploying server and client
   * certificates to allow for secure communication between sonar 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 sonar CA to sign a client certificate which it
   * deploys securely to the app.
   * It also deploys the sonar CA cert to the app.
   * The app can trust a server if and only if it has a certificate signed by the
   * sonar CA.
  */
  export default class CertificateProvider {
    logger: LogManager;
    adb: any;
    certificateSetup: Promise<void>;
    server: Server;
  
    constructor(server: Server, logger: LogManager) {
      this.logger = logger;
      this.adb = adb.createClient();
      this.certificateSetup = this.ensureServerCertExists();
      this.server = server;
    }
  
    processCertificateSigningRequest(
      csr: string,
      os: string,
      appDirectory: string,
    ): Promise<void> {
      this.ensureOpenSSLIsAvailable();
      if (!appDirectory.match(allowedAppDirectoryRegex)) {
        return Promise.reject(
          new Error(
            `Invalid appDirectory recieved from ${os} device: ${appDirectory}`,
          ),
        );
      }
      return this.certificateSetup
        .then(_ => this.getCACertificate())
        .then(caCert =>
          this.deployFileToMobileApp(
            appDirectory,
            deviceCAcertFile,
            caCert,
            csr,
            os,
          ),
        )
        .then(_ => this.generateClientCertificate(csr))
        .then(clientCert =>
          this.deployFileToMobileApp(
            appDirectory,
            deviceClientCertFile,
            clientCert,
            csr,
            os,
          ),
        );
    }
  
    ensureOpenSSLIsAvailable(): void {
      if (!opensslInstalled()) {
        const e = Error(
          "It looks like you don't have OpenSSL installed. Please install it to continue.",
        );
        this.server.emit('error', e);
      }
    }
  
    getCACertificate(): Promise<string> {
      return new Promise((resolve, reject) => {
        fs.readFile(caCert, (err, data) => {
          if (err) {
            reject(err);
          } else {
            resolve(data.toString());
          }
        });
      });
    }
  
    generateClientCertificate(csr: string): Promise<string> {
      this.logger.warn('Creating new client cert', 'CertificateProvider');
      const csrFile = this.writeToTempFile(csr);
      // Create a certificate for the client, using the details in the CSR.
      return openssl('x509', {
        req: true,
        in: csrFile,
        CA: caCert,
        CAkey: caKey,
        CAcreateserial: true,
      }).then(cert => {
        fs.unlinkSync(csrFile);
        return cert;
      });
    }
  
    deployFileToMobileApp(
      destination: string,
      filename: string,
      contents: string,
      csr: string,
      os: string,
    ) {
      if (os === 'Android') {
        this.extractAppNameFromCSR(csr).then(app => {
          const client = adb.createClient();
          client.listDevices().then((devices: Array<{id: string}>) => {
            devices.forEach(d =>
              // To find out which device requested the cert, search them
              // all for a matching csr file.
              // It's not important to keep these secret from other apps.
              // Just need to make sure each app can find it's own one.
              this.androidDeviceHasMatchingCSR(destination, d.id, app, csr)
                .catch(e =>
                  this.logger.error(
                    `Unable to check for matching CSR in ${d.id}:${app}`,
                    'CertificateProvider',
                  ),
                )
                .then(isMatch => {
                  if (isMatch) {
                    this.pushFileToAndroidDevice(
                      d.id,
                      app,
                      destination + filename,
                      contents,
                    );
                  }
                }),
            );
          });
        });
      }
      if (os === 'iOS') {
        fs.writeFileSync(destination + filename, contents);
      }
    }
  
    androidDeviceHasMatchingCSR(
      directory: string,
      deviceId: string,
      processName: string,
      csr: string,
    ): Promise<boolean> {
      return this.executeCommandOnAndroid(
        deviceId,
        processName,
        `cat ${directory + csrFileName}`,
      ).then(deviceCsr => {
        return (
          deviceCsr
            .toString()
            .replace(/\r/g, '')
            .trim() === csr.replace(/\r/g, '').trim()
        );
      });
    }
  
    pushFileToAndroidDevice(
      deviceId: string,
      app: string,
      filename: string,
      contents: string,
    ): Promise<void> {
      this.logger.warn(
        `Deploying sonar certificate to ${deviceId}:${app}`,
        'CertificateProvider',
      );
      return this.executeCommandOnAndroid(
        deviceId,
        app,
        `echo "${contents}" > ${filename} && chmod 600 ${filename}`,
      ).then(output => undefined);
    }
  
    executeCommandOnAndroid(
      deviceId: string,
      user: string,
      command: string,
    ): Promise<string> {
      if (!user.match(allowedAppNameRegex)) {
        return Promise.reject(new Error(`Disallowed run-as user: ${user}`));
      }
      if (command.match(/[']/)) {
        return Promise.reject(
          new Error(`Disallowed escaping command: ${command}`),
        );
      }
      return this.adb
        .shell(deviceId, `echo '${command}' | run-as '${user}'`)
        .then(adb.util.readAll)
        .then(buffer => buffer.toString())
        .then(output => {
          const matches = output.match(appNotDebuggableRegex);
          if (matches) {
            const e = new Error(
              `Android app ${user} is not debuggable. To use it with sonar, add android:debuggable="true" to the application section of AndroidManifest.xml`,
            );
            this.server.emit('error', e);
            throw e;
          }
          return output;
        });
    }
  
    extractAppNameFromCSR(csr: string): Promise<string> {
      const csrFile = this.writeToTempFile(csr);
      return openssl('req', {in: csrFile, noout: true, subject: true})
        .then(subject => {
          fs.unlink(csrFile);
          return subject;
        })
        .then(subject => {
          return subject
            .split('/')
            .filter(part => {
              return part.startsWith('CN=');
            })
            .map(part => {
              return part.split('=')[1].trim();
            })[0];
        })
        .then(appName => {
          if (!appName.match(allowedAppNameRegex)) {
            throw new Error(
              `Disallowed app name in CSR: ${appName}. Only alphanumeric characters and '.' allowed.`,
            );
          }
          return appName;
        });
    }
  
    loadSecureServerConfig(): Promise<SecureServerConfig> {
      return this.certificateSetup.then(() => {
        return {
          key: fs.readFileSync(serverKey),
          cert: fs.readFileSync(serverCert),
          ca: fs.readFileSync(caCert),
          requestCert: true,
          rejectUnauthorized: true, // can be false if necessary as we don't strictly need to verify the client
        };
      });
    }
  
    ensureCertificateAuthorityExists(): Promise<void> {
      if (!fs.existsSync(caKey)) {
        return this.generateCertificateAuthority();
      }
      return this.checkCertIsValid(caCert).catch(e =>
        this.generateCertificateAuthority(),
      );
    }
  
    checkCertIsValid(filename: string): Promise<void> {
      if (!fs.existsSync(filename)) {
        return Promise.reject();
      }
      return openssl('x509', {
        checkend: minCertExpiryWindowSeconds,
        in: filename,
      })
        .then(output => undefined)
        .catch(e => {
          this.logger.warn(
            `Certificate will expire soon: ${filename}`,
            'CertificateProvider',
          );
          throw e;
        });
    }
  
    verifyServerCertWasIssuedByCA() {
      const options = {CAfile: caCert};
      options[serverCert] = false;
      return openssl('verify', options).then(output => {
        const verified = output.match(/[^:]+: OK/);
        if (!verified) {
          // This should never happen, but if it does, we need to notice so we can
          // generate a valid one, or no clients will trust our server.
          throw new Error('Current server cert was not issued by current CA');
        }
      });
    }
  
    generateCertificateAuthority(): Promise<void> {
      if (!fs.existsSync(getFilePath(''))) {
        fs.mkdirSync(getFilePath(''));
      }
      this.logger.info('Generating new CA', 'CertificateProvider');
      return openssl('genrsa', {out: caKey, '2048': false})
        .then(_ =>
          openssl('req', {
            new: true,
            x509: true,
            subj: caSubject,
            key: caKey,
            out: caCert,
          }),
        )
        .then(_ => undefined);
    }
  
    ensureServerCertExists(): Promise<void> {
      if (
        !(
          fs.existsSync(serverKey) &&
          fs.existsSync(serverCert) &&
          fs.existsSync(caCert)
        )
      ) {
        return this.generateServerCertificate();
      }
  
      return this.checkCertIsValid(serverCert)
        .then(_ => this.verifyServerCertWasIssuedByCA())
        .catch(e => this.generateServerCertificate());
    }
  
    generateServerCertificate(): Promise<void> {
      return this.ensureCertificateAuthorityExists()
        .then(_ => {
          this.logger.warn('Creating new server cert', 'CertificateProvider');
        })
        .then(_ => openssl('genrsa', {out: serverKey, '2048': false}))
        .then(_ =>
          openssl('req', {
            new: true,
            key: serverKey,
            out: serverCsr,
            subj: serverSubject,
          }),
        )
        .then(_ =>
          openssl('x509', {
            req: true,
            in: serverCsr,
            CA: caCert,
            CAkey: caKey,
            CAcreateserial: true,
            out: serverCert,
          }),
        )
        .then(_ => undefined);
    }
  
    writeToTempFile(content: string): string {
      const fileName = getFilePath(`deviceCSR-${Math.random() * 1000000}`);
      fs.writeFileSync(fileName, content);
      return fileName;
    }
  }
  
  function getFilePath(fileName: string): string {
    return path.resolve(os.homedir(), '.sonar', 'certs', fileName);
  }
  