深入浅出Electron:原理、工程与实践
上QQ阅读APP看书,第一时间看更新

4.2 如何校验新版本的安装包

前面提到,开发人员制成了新版本的安装包后,需要把相关文件上传到服务器上去,在讲解具体的升级过程前,我们先来看看安装包的描述文件latest.yml,代码如下:

version: 0.0.1
files:
  - url: yourProductName Setup 0.0.1.exe
    sha512: f1Z1IL8GFynwiUWI2sPmwqPeGdXBe5VJeUxpSDsE+AMMXRqfeE7FwlM/9zGO9Kkwd4CweCLuezYzzfw7/721HQ==
    size: 54310823
    isAdminRightsRequired: true
path: yourProductName Setup 0.0.1.exe
sha512: f1Z1IL8GFynwiUWI2sPmwqPeGdXBe5VJeUxpSDsE+AMMXRqfeE7FwlM/9zGO9Kkwd4CweCLuezYzzfw7/721HQ==
releaseDate: '2021-02-25T13:26:18.387Z'

在这个描述文件中有以下几个重要信息:

  • 新版本安装文件的版本号。
  • 新版本安装文件的文件名。
  • 新版本安装文件的sha512值。
  • 新版本安装文件的文件大小。
  • 执行新版本安装文件时是否需要管理员权限。
  • 新版本安装文件的生成时间。

当autoUpdater.checkForUpdates()方法执行时,应用会先请求这个latest.yml文件,得到文件里的内容后,先拿此文件中的版本号与当前版本号对比,如果此文件中的版本号比当前版本号新,则下载新版本,否则则退出更新逻辑。

可能读者会觉得yml文件里的信息看起来毫无章法,electron-updater是如何格式化这些信息的呢?首先yml文件也是类似json的一种标准的文件格式,文档格式说明详见https://yaml.org/,其次electron-updater是基于js-yaml(https://github.com/nodeca/js-yaml)这个库完成yml文件格式化工作的。

另外,版本号的新旧判断法则是根据npm团队定义的SemVer规则(https://semver.org/lang/zh-CN/)执行的,npm团队也专门为此提供了一个库来辅助开发人员完成版本号新旧的判断:https://www.npmjs.com/package/semver,electron-updater就是使用这个库完成此项工作。

在Windows操作系统中,默认情况下新版本安装包会被下载到C:\Users\[yourUser-Name]\AppData\Local\[yourAppName]-updater目录下,Mac环境下新版本安装包会被下载到/Users/[yourUserName]/AppData/Local/[yourAppName]-updater目录下,electron-updater先校验下载的文件是否合法,校验主要通过如下两步工作完成:

1)验证文件的sha512值是否合法,latest.yml文件中包含新版本安装包的sha512值,electron-updater首先计算出下载的新版本安装包的sha512值,然后再与latest.yml文件中的sha512值对比,两个值相等,则验证通过,不相等则验证不通过,计算一个文件的sha512值的代码如下所示:

import { createHash } from "crypto"
import { createReadStream } from "fs"
function hashFile(file: string, algorithm = "sha512", encoding: "base64" | "hex" = "base64", options?: any): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    const hash = createHash(algorithm)
    hash.on("error", reject).setEncoding(encoding)
    createReadStream(file, {...options, highWaterMark: 1024 * 1024})
      .on("error", reject)
      .on("end", () => {
        hash.end()
        resolve(hash.read() as string)
      })
      .pipe(hash, {end: false})
  })
}

上述代码中使用Node.js内置crypto库的createHash方法创建一个哈希对象,接着使用fs库的createReadStream方法读取安装包文件,并把读取流转移到哈希对象内。当读取流读取完成时,文件的sha512哈希值也就计算出来了。

2)验证新版本安装文件的签名是否合法,代码如下所示:

export function verifySignature(publisherNames: Array<string>, unescapedTemp  UpdateFile: string, logger: Logger): Promise<string | null> {
  return new Promise<string | null>(resolve => {
    const tempUpdateFile = unescapedTempUpdateFile.replace(/'/g, "''").replace(/'/g, "''");
    execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-InputFormat", "None", "-Command", 'Get-AuthenticodeSignature '${tempUpdateFile}' | ConvertTo-Json -Compress | ForEach-Object { [Convert]::ToBase64String ([System.Text.Encoding]::UTF8.GetBytes($_)) }'], {
      timeout: 20 * 1000
    }, (error, stdout, stderr) => {
      try {
        if (error != null || stderr) {
          handleError(logger, error, stderr)
          resolve(null)
          return
        }
        const data = parseOut(Buffer.from(stdout, "base64").toString("utf-8"))
        if (data.Status === 0) {
          const name = parseDn(data.SignerCertificate.Subject).get("CN")!
          if (publisherNames.includes(name)) {
            resolve(null)
            return
          }
        }
        const result = 'publisherNames: ${publisherNames.join(" | ")}, raw info: ' + JSON.stringify(data, (name, value) => name === "RawData" ? undefined : value, 2)
        logger.warn('Sign verification failed, installer signed with incorrect certificate: ${result}')
        resolve(result)
      }
      catch (e) {
        logger.warn('Cannot execute Get-AuthenticodeSignature: ${error}. Ignoring signature validation due to unknown error.')
        resolve(null)
        return
      }
    })
  })
}

在上述代码中,electron-updater使用powershell的Get-AuthenticodeSignature命令获取文件的签名信息,如果验证成功则该方法返回null,如果验证失败则该方法返回验证信息。

代码中parseOut是electron-builder仓储中另一个库builder-util-runtime提供的方法,此方法格式化powershell返回的信息详见https://github.com/electron-userland/electron-builder/blob/master/packages/builder-util-runtime/src/rfc2253Parser.ts,此处不再赘述。