<commit_before>import { exec, getTempName } from "./util"
import { deleteFile, outputFile, copy, rename } from "fs-extra-p"
import { download } from "./httpRequest"
import { tmpdir } from "os"
import * as path from "path"
import { executeFinally, all } from "./promise"
import { Promise as BluebirdPromise } from "bluebird"
import { randomBytes } from "crypto"
import { homedir } from "os"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("./awaiter")

export const appleCertificatePrefixes = ["Developer ID Application:", "3rd Party Mac Developer Application:", "Developer ID Installer:", "3rd Party Mac Developer Installer:"]

export type CertType = "Developer ID Application" | "3rd Party Mac Developer Application" | "Developer ID Installer" | "3rd Party Mac Developer Installer"

export interface CodeSigningInfo {
  name: string
  keychainName?: string | null

  installerName?: string | null
}

export function generateKeychainName(): string {
  return path.join(tmpdir(), getTempName("csc") + ".keychain")
}

function downloadUrlOrBase64(urlOrBase64: string, destination: string): BluebirdPromise<any> {
  if (urlOrBase64.startsWith("https://")) {
    return download(urlOrBase64, destination)
  }
  else {
    return outputFile(destination, new Buffer(urlOrBase64, "base64"))
  }
}

let bundledCertKeychainAdded: Promise<any> | null = null

// "Note that filename will not be searched to resolve the signing identity's certificate chain unless it is also on the user's keychain search list."
// but "security list-keychains" doesn't support add - we should 1) get current list 2) set new list - it is very bad http://stackoverflow.com/questions/10538942/add-a-keychain-to-search-list
// "overly complicated and introduces a race condition."
// https://github.com/electron-userland/electron-builder/issues/398
async function createCustomCertKeychain() {
  // copy to temp and then atomic rename to final path
  const tmpKeychainPath = path.join(homedir(), ".cache", getTempName("electron_builder_root_certs"))
  const keychainPath = path.join(homedir(), ".cache", "electron_builder_root_certs.keychain")
  const results = await BluebirdPromise.all<string>([
    exec("security", ["list-keychains"]),
    copy(path.join(__dirname, "..", "certs", "root_certs.keychain"), tmpKeychainPath)
      .then(() => rename(tmpKeychainPath, keychainPath)),
  ])
  const list = results[0]
    .split("\n")
    .map(it => {
      let r = it.trim()
      return r.substring(1, r.length - 1)
    })
    .filter(it => it.length > 0)

  if (!list.includes(keychainPath)) {
    await exec("security", ["list-keychains", "-d", "user", "-s", keychainPath].concat(list))
  }
}

export async function createKeychain(keychainName: string, cscLink: string, cscKeyPassword: string, cscILink?: string | null, cscIKeyPassword?: string | null): Promise<CodeSigningInfo> {
  if (bundledCertKeychainAdded == null) {
    bundledCertKeychainAdded = createCustomCertKeychain()
  }
  await bundledCertKeychainAdded

  const certLinks = [cscLink]
  if (cscILink != null) {
    certLinks.push(cscILink)
  }

  const certPaths = new Array(certLinks.length)
  const keychainPassword = <PASSWORD>(8).<PASSWORD>("hex")
  return await executeFinally(BluebirdPromise.all([
      BluebirdPromise.map(certLinks, (link, i) => {
        const tempFile = path.join(tmpdir(), `${getTempName()}.p12`)
        certPaths[i] = tempFile
        return downloadUrlOrBase64(link, tempFile)
      }),
      BluebirdPromise.mapSeries([
        ["create-keychain", "-p", keychainPassword, keychainName],
        ["unlock-keychain", "-p", keychainPassword, keychainName],
        ["set-keychain-settings", "-t", "3600", "-u", keychainName]
      ], it => exec("security", it))
    ])
    .then(() => importCerts(keychainName, certPaths, <Array<string>>[cscKeyPassword, cscIKeyPassword].filter(it => it != null))),
    errorOccurred => {
      const tasks = certPaths.map(it => deleteFile(it, true))
      if (errorOccurred) {
        tasks.push(deleteKeychain(keychainName))
      }
      return all(tasks)
    })
}

async function importCerts(keychainName: string, paths: Array<string>, keyPasswords: Array<string>): Promise<CodeSigningInfo> {
  const namePromises: Array<Promise<string>> = []
  for (let i = 0; i < paths.length; i++) {
    const password = keyPasswords[i]
    const certPath = paths[i]
    await exec("security", ["import", certPath, "-k", keychainName, "-T", "/usr/bin/codesign", "-T", "/usr/bin/productbuild", "-P", password])

    namePromises.push(extractCommonName(password, certPath))
  }

  const names = await BluebirdPromise.all(namePromises)
  return {
    name: names[0],
    installerName: names.length > 1 ? names[1] : null,
    keychainName: keychainName,
  }
}

function extractCommonName(password: string, certPath: string): BluebirdPromise<string> {
  return exec("openssl", ["pkcs12", "-nokeys", "-nodes", "-passin", "pass:" + password, "-nomacver", "-clcerts", "-in", certPath])
    .then(result => {
      const match = <Array<string | null> | null>(result.match(/^subject.*\/CN=([^\/\n]+)/m))
      if (match == null || match[1] == null) {
        throw new Error("Cannot extract common name from p12")
      }
      else {
        return match[1]!
      }
    })
}

export function sign(path: string, options: CodeSigningInfo): BluebirdPromise<any> {
  const args = ["--deep", "--force", "--sign", options.name, path]
  if (options.keychainName != null) {
    args.push("--keychain", options.keychainName)
  }
  return exec("codesign", args)
}

export function deleteKeychain(keychainName: string, ignoreNotFound: boolean = true): BluebirdPromise<any> {
  const result = exec("security", ["delete-keychain", keychainName])
  if (ignoreNotFound) {
    return result.catch(error => {
      if (!error.message.includes("The specified keychain could not be found.")) {
        throw error
      }
    })
  }
  else {
    return result
  }
}

export function downloadCertificate(cscLink: string): Promise<string> {
  const certPath = path.join(tmpdir(), `${getTempName()}.p12`)
  return downloadUrlOrBase64(cscLink, certPath)
    .thenReturn(certPath)
}

export let findIdentityRawResult: Promise<string> | null = null

export async function findIdentity(namePrefix: CertType, qualifier?: string): Promise<string | null> {
  if (findIdentityRawResult == null) {
    // https://github.com/electron-userland/electron-builder/issues/481
    findIdentityRawResult = exec("security", ["find-identity", "-v", "-p", "codesigning", "-p", "appleID"])
  }

  // https://github.com/electron-userland/electron-builder/issues/484
  const lines = (await findIdentityRawResult)
    .trim()
    .split("\n")
    .filter(it => !it.includes("(Missing required extension)") && !it.includes("valid identities found") && !it.includes("iPhone "))

  for (let line of lines) {<commit_msg>fix: ignore com.apple.idms.appleid.prd. certs<commit_after>import { exec, getTempName } from "./util"
import { deleteFile, outputFile, copy, rename } from "fs-extra-p"
import { download } from "./httpRequest"
import { tmpdir } from "os"
import * as path from "path"
import { executeFinally, all } from "./promise"
import { Promise as BluebirdPromise } from "bluebird"
import { randomBytes } from "crypto"
import { homedir } from "os"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("./awaiter")

export const appleCertificatePrefixes = ["Developer ID Application:", "3rd Party Mac Developer Application:", "Developer ID Installer:", "3rd Party Mac Developer Installer:"]

export type CertType = "Developer ID Application" | "3rd Party Mac Developer Application" | "Developer ID Installer" | "3rd Party Mac Developer Installer"

export interface CodeSigningInfo {
  name: string
  keychainName?: string | null

  installerName?: string | null
}

export function generateKeychainName(): string {
  return path.join(tmpdir(), getTempName("csc") + ".keychain")
}

function downloadUrlOrBase64(urlOrBase64: string, destination: string): BluebirdPromise<any> {
  if (urlOrBase64.startsWith("https://")) {
    return download(urlOrBase64, destination)
  }
  else {
    return outputFile(destination, new Buffer(urlOrBase64, "base64"))
  }
}

let bundledCertKeychainAdded: Promise<any> | null = null

// "Note that filename will not be searched to resolve the signing identity's certificate chain unless it is also on the user's keychain search list."
// but "security list-keychains" doesn't support add - we should 1) get current list 2) set new list - it is very bad http://stackoverflow.com/questions/10538942/add-a-keychain-to-search-list
// "overly complicated and introduces a race condition."
// https://github.com/electron-userland/electron-builder/issues/398
async function createCustomCertKeychain() {
  // copy to temp and then atomic rename to final path
  const tmpKeychainPath = path.join(homedir(), ".cache", getTempName("electron_builder_root_certs"))
  const keychainPath = path.join(homedir(), ".cache", "electron_builder_root_certs.keychain")
  const results = await BluebirdPromise.all<string>([
    exec("security", ["list-keychains"]),
    copy(path.join(__dirname, "..", "certs", "root_certs.keychain"), tmpKeychainPath)
      .then(() => rename(tmpKeychainPath, keychainPath)),
  ])
  const list = results[0]
    .split("\n")
    .map(it => {
      let r = it.trim()
      return r.substring(1, r.length - 1)
    })
    .filter(it => it.length > 0)

  if (!list.includes(keychainPath)) {
    await exec("security", ["list-keychains", "-d", "user", "-s", keychainPath].concat(list))
  }
}

export async function createKeychain(keychainName: string, cscLink: string, cscKeyPassword: string, cscILink?: string | null, cscIKeyPassword?: string | null): Promise<CodeSigningInfo> {
  if (bundledCertKeychainAdded == null) {
    bundledCertKeychainAdded = createCustomCertKeychain()
  }
  await bundledCertKeychainAdded

  const certLinks = [cscLink]
  if (cscILink != null) {
    certLinks.push(cscILink)
  }

  const certPaths = new Array(certLinks.length)
  const keychainPassword = <PASSWORD>Bytes(8).toString("hex")
  return await executeFinally(BluebirdPromise.all([
      BluebirdPromise.map(certLinks, (link, i) => {
        const tempFile = path.join(tmpdir(), `${getTempName()}.p12`)
        certPaths[i] = tempFile
        return downloadUrlOrBase64(link, tempFile)
      }),
      BluebirdPromise.mapSeries([
        ["create-keychain", "-p", keychainPassword, keychainName],
        ["unlock-keychain", "-p", keychainPassword, keychainName],
        ["set-keychain-settings", "-t", "3600", "-u", keychainName]
      ], it => exec("security", it))
    ])
    .then(() => importCerts(keychainName, certPaths, <Array<string>>[cscKeyPassword, cscIKeyPassword].filter(it => it != null))),
    errorOccurred => {
      const tasks = certPaths.map(it => deleteFile(it, true))
      if (errorOccurred) {
        tasks.push(deleteKeychain(keychainName))
      }
      return all(tasks)
    })
}

async function importCerts(keychainName: string, paths: Array<string>, keyPasswords: Array<string>): Promise<CodeSigningInfo> {
  const namePromises: Array<Promise<string>> = []
  for (let i = 0; i < paths.length; i++) {
    const password = keyPasswords[i]
    const certPath = paths[i]
    await exec("security", ["import", certPath, "-k", keychainName, "-T", "/usr/bin/codesign", "-T", "/usr/bin/productbuild", "-P", password])

    namePromises.push(extractCommonName(password, certPath))
  }

  const names = await BluebirdPromise.all(namePromises)
  return {
    name: names[0],
    installerName: names.length > 1 ? names[1] : null,
    keychainName: keychainName,
  }
}

function extractCommonName(password: string, certPath: string): BluebirdPromise<string> {
  return exec("openssl", ["pkcs12", "-nokeys", "-nodes", "-passin", "<PASSWORD>:" + password, "-nomacver", "-clcerts", "-in", certPath])
    .then(result => {
      const match = <Array<string | null> | null>(result.match(/^subject.*\/CN=([^\/\n]+)/m))
      if (match == null || match[1] == null) {
        throw new Error("Cannot extract common name from p12")
      }
      else {
        return match[1]!
      }
    })
}

export function sign(path: string, options: CodeSigningInfo): BluebirdPromise<any> {
  const args = ["--deep", "--force", "--sign", options.name, path]
  if (options.keychainName != null) {
    args.push("--keychain", options.keychainName)
  }
  return exec("codesign", args)
}

export function deleteKeychain(keychainName: string, ignoreNotFound: boolean = true): BluebirdPromise<any> {
  const result = exec("security", ["delete-keychain", keychainName])
  if (ignoreNotFound) {
    return result.catch(error => {
      if (!error.message.includes("The specified keychain could not be found.")) {
        throw error
      }
    })
  }
  else {
    return result
  }
}

export function downloadCertificate(cscLink: string): Promise<string> {
  const certPath = path.join(tmpdir(), `${getTempName()}.p12`)
  return downloadUrlOrBase64(cscLink, certPath)
    .thenReturn(certPath)
}

export let findIdentityRawResult: Promise<string> | null = null

export async function findIdentity(namePrefix: CertType, qualifier?: string): Promise<string | null> {
  if (findIdentityRawResult == null) {
    // https://github.com/electron-userland/electron-builder/issues/481
    findIdentityRawResult = exec("security", ["find-identity", "-v", "-p", "codesigning", "-p", "appleID"])
  }

  // https://github.com/electron-userland/electron-builder/issues/484
  //noinspection SpellCheckingInspection
  const lines = (await findIdentityRawResult)
    .trim()
    .split("\n")
    .filter(it => !it.includes("(Missing required extension)") && !it.includes("valid identities found") && !it.includes("iPhone ") && !it.includes("com.apple.idms.appleid.prd."))

  for (let line of lines) {
{'fraction_non_alphanumeric': 0.1480545730166751, 'fraction_numerical': 0.008951129719194399, 'mean_word_length': 4.194600674915636, 'pattern_counts': {'":': 0, '<': 47, '<?xml version=': 0, '>': 73, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 28, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4378835', 'n_tokens_mistral': 4374, 'n_tokens_neox': 4232, 'n_words': 1072}