大家好呀,时隔两月,今天又来学习和研究figma客户端了。不管是从什么角度去揣测,分析,今天不如更直接一些,看代码!虽然是压缩混淆过的。
过程仅供交流学习
过程仅供交流学习
过程仅供交流学习

那么,我们需要做些什么呢?

项目结构

在Window系统中,右键跳转到文件所在目录,就可以很清晰的看到应用的结构,有exe入口执行文件,依赖的资源和动态链接库等等,可是在MacOS呢?
打开访达(finder)—— 左侧选中应用程序 —— 右键对应的应用(如Figma)—— 显示包内容
就可以看到应用的构成了,接着打开终端,把Content拖到终端里面,可以看到类似/Applications/Figma.app/Contents的路径,也就是说,通过移动到Applications的安装方式,会出现在这里,使用.app后缀的目录。
tree指令输出下Content

.
├── CodeResources
├── Info.plist
├── MacOS
│   └── DynamicUniversalApp
├── PkgInfo
├── Resources
│   ├── App.nib
│   ├── en.lproj
│   ├── icon.icns
│   └── ja.lproj
└── _CodeSignature
    └── CodeResources

(这里是还未第一次启动的figma,在启动 DynamicUniversalApp 之后,才会真正的去下载客户端的应用。猜测是为了避免用户安装到其它的文件目录,导致持久化信息、目录访问受限、FigmaAgent无法安装等问题。毕竟在应用拖往Application的弹窗中直接双击启动,对应的根目录和正常启动的是不一致的)
安装好之后的tree结构如下

.
├── CodeResources
├── Frameworks
│   ├── Electron Framework.framework
│   ├── Figma Helper (GPU).app
│   ├── Figma Helper (Plugin).app
│   ├── Figma Helper (Renderer).app
│   ├── Figma Helper.app
│   ├── Mantle.framework
│   ├── ReactiveObjC.framework
│   └── Squirrel.framework
├── Info.plist
├── Library
│   ├── FigmaAgent.app
│   └── QuickLook
├── MacOS
│   ├── DynamicUniversalApp
│   └── Figma
├── PkgInfo
├── Resources
│   ├── App.nib
│   ├── af.lproj
│   ├── ...
│   ├── app.asar
│   ├── app.asar.unpacked
│   ├── ...
└── _CodeSignature
    └── CodeResources

可以看到在MacOS下增加了客户端的入口文件,Resource多出了app.asarapp.asar.unpacked文件和目录。

asar是什么?

简单来说,是在 Electron 里用来打包应用资源文件的一种特殊格式。它能把好多文件和文件夹合成一个文件,这样能让应用的资源分发和加载更便捷高效。而且它能保持原来文件系统的结构和内容,既能减少文件数量,又能在使用时快速读取文件,还能增加应用资源的安全性

其实就是业务逻辑代码,会集中在这里,也就是今天讨论的重点。

既然在运行时候需要获取app.asar的内容,那么Electron是怎么做到的呢?找到Electron package.json,devDependencies依赖了"@electron/asar": "^3.2.1"这个包,再去npm搜索,对应的是github.com/electron/asar远端项目。

当然,现在需要单独的处理asar,而不是在Electron下。找了下社区,就有方案,使用asar这个包即可,去npm查看,对应的同一个远端项目。这里就没问题,直接全局安装npm install asar -g

asar npm address.png

解压asar

这样我们就可以参照文档,尝试解压figma的asar了。

# 进入目录
cd /Applications/Figma.app/Contents/Resources
# 解压
asar extract app.asar ./app

不出意外,就会有意外。报错了

Error: EPERM: operation not permitted, mkdir ‘./app’

这个是没有授权终端创建目录。在【隐私与安全性】的【APP管理】,允许更新删除就好

The value of “size” is out of range
node:internal/errors:541
      throw error;
      ^

RangeError [ERR_OUT_OF_RANGE]: The value of "size" is out of range. It must be >= 0 && <= 4294967296. Received -1000
    at Function.alloc (node:buffer:389:3)
    at module.exports.readFileSync (/xxx/.nvm/versions/node/v20.12.2/lib/node_modules/asar/lib/disk.js:106:23)
    at module.exports.extractAll (/xxx/.nvm/versions/node/v20.12.2/lib/node_modules/asar/lib/asar.js:204:28)

直接就被硬控了,难道包出问题了?!

其它项目尝试

拿了个其它项目来尝试

# 解压
asar extract app.asar ./app
# 备份
mv app.asar app-backup.asar 
# 压缩
asar pack ./app ./app.asar

执行这个过程没有任何报错,执行MacOS的入口文件也都能正常的运行。只是app.asar比原有的包更大了(有app.asar.unpacked),原因是解压的目录会包含unpack,重新打包的时候包含进去。

修改 asar 包代码

没办法,只好去查看报错的代码了。

module.exports.readFileSync = function (filesystem, filename, info) {
  let buffer = Buffer.alloc(info.size)
  if (info.size <= 0) { return buffer }
  // ...
  return buffer
}

申请buffer时候超出范围了。info.size是传入的参数,继续往下

  // ...
  const filesystem = disk.readFilesystemSync(archive)
  // ...
  for (const fullPath of filenames) {
    // Remove leading slash
    const filename = fullPath.substr(1)
    const destFilename = path.join(dest, filename)
    const file = filesystem.getFile(filename, followLinks)
    if (file.files) {
      // ...
    } else {
      // it's a file, extract it
      const content = disk.readFileSync(filesystem, filename, file)
      // ...
    }
  }

file信息是通过filesystem.getFile(filename, followLinks)到的,filesystem又是通过const filesystem = disk.readFilesystemSync(archive)得到的。 这下好好看看,这一步到底做了什么事情。
几次console调整之后。

module.exports.readFilesystemSync = function (archive) {
  if (!filesystemCache[archive]) {
    const header = this.readArchiveHeaderSync(archive)
    console.info('获取到的header元数据:', JSON.parse(header.headerString));
    const filesystem = new Filesystem(archive)
    // ...
  }
  return filesystemCache[archive]
}
// 重新跑 asar extract app.asar ./app ,得到输出
// 获取到的header元数据: {
//   files: {
//     '.codesign': { size: -1000, offset: '0', integrity: [Object] },
//     assets: { files: [Object] },
//     'bindings.node': { size: 139808, unpacked: true, integrity: [Object] },
//     'build.json': { size: 26, offset: '477349', integrity: [Object] },
//     'desktop_rust.node': { size: 2280008, unpacked: true, integrity: [Object] },
//     'desktop_shell.css': { size: 323004, offset: '477375', integrity: [Object] },
//     'desktop_shell.js': { size: 373725, offset: '800379', integrity: [Object] },
//     i18n: { files: [Object] },
//     loading_screen: { files: [Object] },
//     'main.js': { size: 467065, offset: '1200404', integrity: [Object] },
//     node_modules: { files: [Object] },
//     'package-lock.json': { size: 16134, offset: '2069716', integrity: [Object] },
//     'package.json': { size: 536, offset: '2085850', integrity: [Object] },
//     'shell_app_binding_renderer.js': { size: 2180, offset: '2086386', integrity: [Object] },
//     tray: { files: [Object] },
//     'web_app_binding_renderer.js': { size: 16626, offset: '2089567', integrity: [Object] }
//   }
// }

好你个浓眉大眼的.codesign,还是个隐藏文件。看偏移量offset: '0',打头的就是size出错的数据。
为避免其它情况下使用asar出错,判断下这个filename,直接给你丫跳过。
这样就可以解压出来内容了,虽然是压缩混淆的,有需要的硬翻也可以翻出来不少东西,后续怎么处理?过程仅供交流学习

到这里,过程就结束了,也就是为啥前面描述asar的时候,我把安全这个给划掉了,安全不了一点儿啊。但是figma的这个做法,确实也能增加一道门槛,文件名都非常直白了。

最后

总的来说,就是用了个asar工具去解压包,碰到了个小错误,改完之后就能解压了。
今天研究asar代码就到这儿,希望对你有帮助😉

更多一点儿

.codesign 真实 size 有多大?

很简单,offset从0开始,只要找到了第二个文件的起始点,那它就是这个文件的大小。
通过上面获取到的元数据信息,发现有的包含了offset,而有的没有。没有的是因为unpack了,没有存在app.asar这个包。
但是,这第二个assets是个目录!刚想递归遍历,但是直接输出下这个详情就好了。

# console
  'Inter.var.woff2': {
    size: 319784,
    offset: '0',
    integrity: {
      algorithm: 'SHA256',
      hash: '0f409d1d652d526bcfd7fd0c2cae003cd1b32a009e71f7f9e614f644bd1d8f52',
      blockSize: 4194304,
      blocks: [Array]
    }
  },

offset依旧是0,惊不惊喜意不意外~。


过程仅供交流学习
过程仅供交流学习
过程仅供交流学习