隐域-项目分析

1 项目简介

1.1 项目简介

「隐域」(英文名Gcrypt)是一款数据加密软件,简单易用,能够较好地保护那些你不想让外界得知的数据。涉及到加密的核心部分基于nodejs的crypto模块,软件的GUI则基于Electron+Vue。目前还只是一个仅供我自己使用的小项目,代码量在1.1w行左右,大部分代码在我高中时期完成。一个典型的加密库至少包含entry.json文件作为入口点。

1.2 项目起因

高中阶段,我有一批涉及隐私的数据急需加密存放。我曾经考虑过veracrypt这一成熟的解决方案,但是其创建的加密卷是单文件(也许是考虑到安全性吧),不利于坚果云这类同步软件进行同步与备份。在一番思考下,我决定使用自己熟悉的技术去从头开发一款类似的数据加密软件,勉强能用即可,同时还能够拓展我的技术栈、提升我的技术熟练度。因此,「隐域」项目就在这一天诞生了。

2 项目分析

2.1 项目文件结构

隐域的项目文件结构严格遵守electron-vue模式。

components目录下存储用于构建用户界面的各类组件,包括高性能虚拟列表、顶部基础工具栏(由每个需要使用工具栏的父组件独立维护,当需要全局模态时,通过Teleport API 传送至App顶层)、右键菜单、代码编辑器、canvas图表等。

store目录负责基于Pinia的全局状态管理,包括设置、加密、任务管理。

styles目录维护了全局less样式文件,包括对vuetify的覆写。

test目录负责单元测试。

.github目录存储了github actions的workflow脚本,在commit后自动在云端进行项目的构建和部署。

api目录是该项目的核心部分,见2.2。


2.2 核心架构

2.2.1 核心实现架构

我从零开始实现了一个内置的独立文件系统,来存储用户存放在加密库中的数据(包括文件夹)。正因为这个文件系统是私有的,所以还要必须提供一个内置的文件管理器,来管理加密库中的内容:

从数据的解密,到文件系统的构建,再到文件列表的展示,隐域对其进行了层层抽象,以求降低代码复杂度和耦合度、便于进行各层implements的切换。

  • 加密引擎(EncryptionEngine):基于crypto,目前支持AES-192算法,对外提供encrypt()decrypt()接口,负责Buffer数据的加解密和初始化向量(iv)的维护。

  • 键值对引擎(KVPEngine):基于加密引擎和本地文件系统,提供扁平化的、安全且持久的key-value操作。此外,还支持自动整合若干小文件为大文件,以提升系统IO性能,这个过程对于上层来说是无感知的。

  • 内建文件系统:基于键值对引擎而构造出来的内置文件系统,使用目录-目录项的方式进行文件的链式索引,不容易因为目录层级的增加而导致性能损失。

  • 适配器(Adapter):向上层提供基础且统一的文件操作接口,如获取当前目录下的文件列表、文件的读写等。

  • 文件管理器(FileMgr):是一个Vue组件,基于适配器提供的接口,负责用户界面的渲染和处理用户交互,可以理解为适配器的shell。


2.2.2 工具类

为了方便实现各类功能、复用相同逻辑,我引入了若干工具类:

  • File类:对文件的二次封装,虽然被封装的文件来源各异(可以来自本地磁盘、内存Buffer、适配器、甚至是Vue的Ref对象),但是File类确保了其对外接口的统一(读写、导出、销毁等),这为不同组件之间的文件传递提供了非常方便且一致的接口。例如,内置的文件管理器在用户选中文件并打开时,会基于当前Adapter实例化一个File类,将其通过全局的openFile()方法提交给打开方式管理器(OpenMethodMgr),并以列表形式显示可用的打开方式,供用户选择。用户选择后,File对象会被提交给对应的组件(作为其props)。由于File对象的接口统一,不同的打开方式所对应的组件均能够接收该对象。(目前支持的打开方式有:内置的文本编辑器、内置的代码编辑器、内置的MD5与SHA-1哈希生成器、内置的图片查看器、内置的HTML查看器等)
  • FileWatcher类:对NodeJS文件监听API fs.watch 的二次封装,在其基础上增加了防抖和节流特性,更加节约资源;还内置了一系列hook以便调用。使用abortController.abort()进行销毁。
  • SecureBuffer类:提供更加安全的缓冲区,让Buffer的分配和销毁都更加安全可控。将在未来引入。
  • Task类:表示一个任务,用于任务管理。在本项目中,文件的加密、删除、导出等操作被封装在了一个个Task对象内,由全局任务管理器(TaskMgr)进行统一管理和调度。每个Task具有'pending' | 'running' | 'succeed' | 'failed' | 'cancelled'五个状态,会被分配一个guidgroupId(任务群组号)。每个Task还可以具有子任务。