如何实现加解密下载文件
===========

#### 问题现象

如何实现文件下载后加密存储到公共目录，使之无法打开查看，需要再次读取公共目录保存的加密文件进行解密后再次保存，方可正常查看？

#### 背景知识

*   [@ohos.file.fs (文件管理)](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-file-fs)：该模块为基础文件操作API，提供基础文件操作能力，包括文件基本管理、文件目录管理、文件信息统计、文件流式读写等常用功能。
*   [加解密开发指导](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/crypto-encrypt-decrypt-dev)：在数据存储或传输场景中，可以使用加解密操作用于保证数据的机密性，防止敏感数据泄露。
*   [DocumentViewPicker](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-file-picker#documentviewpicker)：适用于文件类型文件的选择与保存。DocumentViewPicker对接的选择资源来自于FilePicker，负责文件类型的资源管理，文件类型不区分后缀，比如浏览器下载的图片、文档等，都属于文件类型。通过Picker访问相关文件，将拉起对应的应用，引导用户完成界面操作，接口本身无需申请权限。

#### 解决方案

由问题可知实现此功能重点是如下两步：

1.  文件读取并加密后保存至公共目录；
2.  选择公共目录下的加密文件解密后，再次保存至公共目录。

**示例代码如下**（代码中使用AES对称密钥（ECB模式）加解密，亦可选择其他加解密算法；前置步骤需要配置网络权限已省略，请参考[配置网络权限](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/graphics-accelerate-assetdownload-prepare#section49951359134817)）：

import { BusinessError, request } from '@kit.BasicServicesKit';
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
import { cryptoFramework } from '@kit.CryptoArchitectureKit';

let keyData = new Uint8Array(\[83, 217, 231, 76, 28, 113, 23, 219, 250, 71, 209, 210, 205, 97, 32, 159\]);
let symKey = genSymKeyByData(keyData);

function genSymKeyByData(symKeyData: Uint8Array) {
  let symKeyBlob: cryptoFramework.DataBlob = { data: symKeyData };
  let aesGenerator = cryptoFramework.createSymKeyGenerator('AES128');
  let symKey = aesGenerator.convertKeySync(symKeyBlob);
  return symKey;
}

_// 加密消息_
function encryptMessage(symKey: cryptoFramework.SymKey, plainText: cryptoFramework.DataBlob) {
  let cipher = cryptoFramework.createCipher('AES128|ECB|PKCS7');
  cipher.initSync(cryptoFramework.CryptoMode.ENCRYPT\_MODE, symKey, null); _// ECB模式params为null。_
  let cipherData = cipher.doFinalSync(plainText);
  return cipherData;
}

_// 解密消息_
function decryptMessage(symKey: cryptoFramework.SymKey, cipherText: cryptoFramework.DataBlob) {
  let decoder = cryptoFramework.createCipher('AES128|ECB|PKCS7');
  decoder.initSync(cryptoFramework.CryptoMode.DECRYPT\_MODE, symKey, null); _// ECB模式params为null。_
  let decryptData = decoder.doFinalSync(cipherText);
  return decryptData;
}

function getPlainText(file: fs.File) {
  let stat = fs.statSync(file.path); _// 获取文件状态_
  let buf = new ArrayBuffer(stat.size); _// 创建一个ArrayBuffer对象_
  let num = fs.readSync(file.fd, buf); _// 读取文件_
  let plainText: cryptoFramework.DataBlob = { data: new Uint8Array(buf.slice(0, num)) };
  return plainText;
}

@Entry
@Component
export struct DocumentSave {
  context = this.getUIContext().getHostContext() as Context;
  _// downloadUrl替换为实际文件下载链接_
  downloadUrl: string = 'https://xxx/xxx/xx.docx';

 _ // 下载文件并加密内容_
  downloadFileAndEncrypt() {
   _ // 自定义文件名以及文件格式_
    const fileName = \`test${Date.now()}.docx\`;
    request.downloadFile(this.context, {
      enableMetered: true,
      url: this.downloadUrl,
      filePath: \`${this.context.filesDir}/${fileName}\`, _// 沙箱路径_
    }).then((downloadTask: request.DownloadTask) => {
      downloadTask.on('complete', () => {
        const documentSaveOptions = new picker.DocumentSaveOptions(); _// 创建文件管理器保存选项实例_
        documentSaveOptions.newFileNames = \[fileName\]; _// 保存文件名_
        const documentViewPicker = new picker.DocumentViewPicker;
        documentViewPicker.save(documentSaveOptions)
          .then(async (documentSaveResult) => {
           _ // 获取到图片或者视频文件的URI后进行文件读取等操作_
            let uri = documentSaveResult\[0\];
            if (uri) {
             _ // 沙箱路径文件_
              let sanFile =
                fs.openSync(\`${this.context.filesDir}/${fileName}\`, fs.OpenMode.READ\_WRITE | fs.OpenMode.CREATE);
              let pubFile = fs.openSync(uri, fs.OpenMode.READ\_WRITE | fs.OpenMode.CREATE);
              const plainText = getPlainText(sanFile);
             _ // 加密_
              let encryptText = encryptMessage(symKey, plainText);
              fs.writeSync(pubFile.fd, encryptText.data.buffer);
              fs.unlink(sanFile.path);
            } else {
            _  // 取消保存到用户目录之后，删除沙箱目录下保存的文件_
              fs.rmdirSync(\`${this.context.filesDir}/${fileName}\`);
            }
          }
          );
      });
    }
    ).catch((err: BusinessError) => {
      console.error(\`Invoke downloadTask failed, code is ${err.code}, message is ${err.message}\`);
    });
  }

  _// 文件解密_
  async decryptFile() {
    let uri: string = '';
   _ // 自定义文件名以及文件格式_
    const fileName = \`test${Date.now()}.docx\`;
    let documentSelectOptions = new picker.DocumentSelectOptions();
    documentSelectOptions.fileSuffixFilters = \[\];
    documentSelectOptions.maxSelectNumber = 1;
    let documentViewPicker = new picker.DocumentViewPicker();
    await documentViewPicker.select(documentSelectOptions).then((documentSelectResult: Array<string>) => {
      uri = decodeURIComponent(documentSelectResult\[0\]);
      let pubFile = fs.openSync(uri, fs.OpenMode.READ\_ONLY);
      let file =
        fs.openSync(\`${this.context.filesDir}/${fileName}\`, fs.OpenMode.READ\_WRITE | fs.OpenMode.CREATE); _// 打开文件_

      const plainText = getPlainText(pubFile);
      let decryptText = decryptMessage(symKey, plainText);
      fs.writeSync(file.fd, decryptText.data.buffer);
      const documentSaveOptions = new picker.DocumentSaveOptions(); _// 创建文件管理器保存选项实例_
      documentSaveOptions.newFileNames = \[fileName\];
      documentViewPicker.save(documentSaveOptions).then(async (documentSaveResult) => {
        let uri = documentSaveResult\[0\];
        if (uri) {
          let pubFile = fs.openSync(uri, fs.OpenMode.READ\_WRITE | fs.OpenMode.CREATE);
          fs.copyFileSync(file.fd, pubFile.fd);
          fs.unlink(file.path);
        } else {
          fs.rmdirSync(\`${this.context.filesDir}/${fileName}\`);
        }
      });
    }).catch((err: Error) => {
      console.error(\`Invoke documentViewPicker.select failed, code is ${err}, message is ${err.message}\`);
    });
  }

  build() {
    RelativeContainer() {
      Row() {
        Button('下载加密保存')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.downloadFileAndEncrypt();
          });
        Button('解密后保存')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.decryptFile();
          });
      };
    }
    .height('100%')
    .width('100%');
  }
}