引言:移动端文件安全的重要性与挑战

在移动应用开发中,文件安全存储一直是个重要但容易被忽视的课题。想象一下这样的场景:AI旅行助手应用需要下载用户的旅行路线规划、酒店预订确认单等敏感文档,如果直接保存到公共目录,任何有文件管理器权限的应用都可以访问这些隐私文件。更糟糕的是,如果设备丢失,敏感信息就会直接暴露。

有开发者可能会问:"为什么不直接存到应用私有目录?"问题在于,用户常常需要在不同应用间分享这些文件,比如将机票PDF通过邮件发送,或者将行程单导入到日历应用。私有目录虽然安全,但无法实现跨应用分享。

这就是我们今天要解决的难题:如何在HarmonyOS中实现文件下载后的加密存储,确保文件在公共目录中无法被直接查看,只有经过授权的应用才能解密访问?

一、核心需求与预期效果

1.1 实际应用场景

在AI旅行助手应用中,文件安全存储功能至关重要:

  1. 行程单保护:用户下载的机票、酒店预订单、景点门票等包含个人身份信息

  2. 隐私政策文件:应用更新的隐私政策、用户协议等需要安全保存

  3. 离线地图数据:下载的离线地图包需要防止被未授权访问

  4. 用户数据备份:旅行日志、照片描述等用户生成内容需要加密备份

1.2 预期效果

用户点击下载敏感文件时,系统应该实现:

文件下载 → 内存中加密 → 加密存储到公共目录 → 文件不可直接打开
↓
用户需要查看时 → 读取加密文件 → 内存中解密 → 保存到临时目录 → 正常打开

整个流程对用户来说应该是无缝的:下载时自动加密,查看时自动解密,用户无感知地享受安全保护。

二、技术架构设计

2.1 系统架构概览

基于HarmonyOS的安全体系,我们设计了三层安全架构:

┌─────────────────────────────────────┐
│          应用层 (业务逻辑)           │
│  • 文件下载请求                      │
│  • 用户权限管理                      │
│  • 加解密触发控制                    │
└───────────────┬─────────────────────┘
                │
┌───────────────▼─────────────────────┐
│          安全服务层                   │
│  • AES加解密引擎                     │
│  • 密钥管理服务                      │
│  • 文件完整性校验                    │
└───────────────┬─────────────────────┘
                │
┌───────────────▼─────────────────────┐
│          存储层                      │
│  • 公共目录访问                      │
│  • 加密文件存储                      │
│  • 临时文件清理                      │
└─────────────────────────────────────┘

2.2 核心API与组件

实现该功能需要以下关键HarmonyOS API:

API类别

具体API

作用说明

文件操作

@ohos.file.fs

文件读写、目录管理

加解密

@ohos.security.cryptoFramework

AES等加密算法实现

网络下载

@ohos.request

文件下载功能

权限管理

@ohos.abilityAccessCtrl

存储权限申请

临时文件

@ohos.file.fileManager

临时文件管理

三、完整实现方案

3.1 基础环境配置

首先配置必要的权限和依赖:

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_USER_STORAGE",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.READ_USER_STORAGE",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "always"
        }
      }
    ]
  }
}

3.2 加密存储管理器实现

创建统一的安全文件管理类:

// FileSecurityManager.ts
import { cryptoFramework } from '@ohos.security.cryptoFramework';
import fs from '@ohos.file.fs';
import { BusinessError } from '@ohos.base';

/**
 * 文件安全管理器
 * 负责文件的加密存储和解密读取
 */
export class FileSecurityManager {
  private static instance: FileSecurityManager;
  private algorithm = 'AES256|ECB|PKCS7';
  private key: cryptoFramework.SymKey | null = null;
  
  // 单例模式
  public static getInstance(): FileSecurityManager {
    if (!FileSecurityManager.instance) {
      FileSecurityManager.instance = new FileSecurityManager();
    }
    return FileSecurityManager.instance;
  }
  
  /**
   * 初始化加密密钥
   */
  async initialize(): Promise<void> {
    try {
      // 生成或获取AES密钥
      this.key = await this.generateAESKey();
      console.info('FileSecurityManager initialized successfully');
    } catch (error) {
      console.error('Failed to initialize FileSecurityManager:', error);
      throw error;
    }
  }
  
  /**
   * 生成AES密钥
   */
  private async generateAESKey(): Promise<cryptoFramework.SymKey> {
    try {
      const symKeyGenerator = cryptoFramework.createSymKeyGenerator('AES256');
      return await symKeyGenerator.generateSymKey();
    } catch (error) {
      console.error('Failed to generate AES key:', error);
      throw error;
    }
  }
  
  /**
   * 加密文件并保存到公共目录
   */
  async encryptAndSaveFile(
    fileData: Uint8Array, 
    fileName: string, 
    relativePath?: string
  ): Promise<string> {
    try {
      // 1. 加密文件数据
      const encryptedData = await this.encryptData(fileData);
      
      // 2. 构建存储路径
      const savePath = await this.buildSavePath(fileName, relativePath);
      
      // 3. 写入加密文件
      await this.writeEncryptedFile(encryptedData, savePath);
      
      // 4. 记录文件元信息(可选)
      await this.saveFileMetadata(fileName, savePath);
      
      return savePath;
    } catch (error) {
      console.error('Failed to encrypt and save file:', error);
      throw error;
    }
  }
  
  /**
   * 读取并解密文件
   */
  async readAndDecryptFile(filePath: string): Promise<Uint8Array> {
    try {
      // 1. 读取加密文件
      const encryptedData = await this.readEncryptedFile(filePath);
      
      // 2. 解密数据
      const decryptedData = await this.decryptData(encryptedData);
      
      return decryptedData;
    } catch (error) {
      console.error('Failed to read and decrypt file:', error);
      throw error;
    }
  }
  
  /**
   * 解密文件并保存到临时目录(可正常查看)
   */
  async decryptToTempFile(encryptedFilePath: string): Promise<string> {
    try {
      // 1. 解密文件数据
      const decryptedData = await this.readAndDecryptFile(encryptedFilePath);
      
      // 2. 创建临时文件
      const tempDir = this.getTempDirectory();
      const tempFileName = `decrypted_${Date.now()}_${this.getFileName(encryptedFilePath)}`;
      const tempFilePath = `${tempDir}/${tempFileName}`;
      
      // 3. 写入临时文件
      await this.writeTempFile(decryptedData, tempFilePath);
      
      // 4. 设置文件权限(仅当前应用可访问)
      await this.setFilePermissions(tempFilePath);
      
      return tempFilePath;
    } catch (error) {
      console.error('Failed to decrypt to temp file:', error);
      throw error;
    }
  }
  
  /**
   * 数据加密核心方法
   */
  private async encryptData(data: Uint8Array): Promise<Uint8Array> {
    if (!this.key) {
      throw new Error('Encryption key not initialized');
    }
    
    try {
      const cipher = cryptoFramework.createCipher(this.algorithm);
      await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, this.key, null);
      
      const encryptData = await cipher.doFinal(data);
      return encryptData.data;
    } catch (error) {
      console.error('Encryption failed:', error);
      throw error;
    }
  }
  
  /**
   * 数据解密核心方法
   */
  private async decryptData(encryptedData: Uint8Array): Promise<Uint8Array> {
    if (!this.key) {
      throw new Error('Encryption key not initialized');
    }
    
    try {
      const decoder = cryptoFramework.createCipher(this.algorithm);
      await decoder.init(cryptoFramework.CryptoMode.DECRYPT_MODE, this.key, null);
      
      const decryptData = await decoder.doFinal(encryptedData);
      return decryptData.data;
    } catch (error) {
      console.error('Decryption failed:', error);
      throw error;
    }
  }
  
  /**
   * 构建文件保存路径
   */
  private async buildSavePath(fileName: string, relativePath?: string): Promise<string> {
    const context = getContext(this) as common.UIAbilityContext;
    const filesDir = context.filesDir;
    
    // 使用加密文件扩展名
    const encryptedFileName = `${fileName}.encrypted`;
    
    if (relativePath) {
      const fullPath = `${filesDir}/${relativePath}`;
      
      // 确保目录存在
      await this.ensureDirectoryExists(fullPath);
      return `${fullPath}/${encryptedFileName}`;
    }
    
    return `${filesDir}/${encryptedFileName}`;
  }
  
  /**
   * 确保目录存在
   */
  private async ensureDirectoryExists(dirPath: string): Promise<void> {
    try {
      const isExist = await fs.access(dirPath);
      if (!isExist) {
        await fs.mkdir(dirPath, true);
      }
    } catch (error) {
      // 目录不存在,创建它
      await fs.mkdir(dirPath, true);
    }
  }
  
  /**
   * 获取临时目录
   */
  private getTempDirectory(): string {
    const context = getContext(this) as common.UIAbilityContext;
    return context.tempDir;
  }
  
  /**
   * 从路径中提取文件名
   */
  private getFileName(filePath: string): string {
    return filePath.substring(filePath.lastIndexOf('/') + 1);
  }
}

3.3 文件下载与加密组件

实现支持加密的文件下载器:

// SecureFileDownloader.ets
import { FileSecurityManager } from './FileSecurityManager';
import { BusinessError } from '@ohos.base';

@Entry
@Component
struct SecureFileDownloader {
  @State downloadProgress: number = 0;
  @State isDownloading: boolean = false;
  @State downloadStatus: string = '等待下载';
  @State downloadedFiles: Array<FileItem> = [];
  
  private fileSecurityManager = FileSecurityManager.getInstance();
  
  // 文件项接口
  interface FileItem {
    id: string;
    name: string;
    encryptedPath: string;
    decryptedPath?: string;
    size: number;
    downloadTime: number;
    isDecrypted: boolean;
  }
  
  aboutToAppear(): void {
    // 初始化文件安全管理器
    this.fileSecurityManager.initialize().catch((err: BusinessError) => {
      console.error('Failed to initialize file security manager:', err);
      prompt.showToast({
        message: '安全服务初始化失败',
        duration: 3000
      });
    });
    
    // 加载已下载文件列表
    this.loadDownloadedFiles();
  }
  
  /**
   * 下载并加密文件
   */
  async downloadAndEncryptFile(url: string, fileName: string): Promise<void> {
    if (this.isDownloading) {
      prompt.showToast({
        message: '正在下载其他文件,请稍后',
        duration: 2000
      });
      return;
    }
    
    this.isDownloading = true;
    this.downloadStatus = '开始下载...';
    this.downloadProgress = 0;
    
    try {
      // 1. 下载文件
      const fileData = await this.downloadFile(url);
      
      // 2. 加密并保存
      this.downloadStatus = '加密文件中...';
      const encryptedPath = await this.fileSecurityManager.encryptAndSaveFile(
        fileData, 
        fileName,
        'encrypted_documents'
      );
      
      // 3. 添加到下载列表
      const fileItem: FileItem = {
        id: `file_${Date.now()}`,
        name: fileName,
        encryptedPath,
        size: fileData.byteLength,
        downloadTime: Date.now(),
        isDecrypted: false
      };
      
      this.downloadedFiles = [fileItem, ...this.downloadedFiles];
      this.saveDownloadedFiles();
      
      this.downloadStatus = '下载完成';
      this.downloadProgress = 100;
      
      prompt.showToast({
        message: `文件"${fileName}"已安全保存`,
        duration: 3000
      });
      
    } catch (error) {
      console.error('Download and encrypt failed:', error);
      this.downloadStatus = '下载失败';
      
      prompt.showToast({
        message: '文件下载失败,请重试',
        duration: 3000
      });
    } finally {
      this.isDownloading = false;
      
      // 2秒后重置状态
      setTimeout(() => {
        this.downloadProgress = 0;
        this.downloadStatus = '等待下载';
      }, 2000);
    }
  }
  
  /**
   * 下载文件实现
   */
  private async downloadFile(url: string): Promise<Uint8Array> {
    return new Promise((resolve, reject) => {
      // 模拟下载过程
      let progress = 0;
      const interval = setInterval(() => {
        progress += 10;
        this.downloadProgress = progress;
        
        if (progress >= 100) {
          clearInterval(interval);
          
          // 模拟下载完成,返回测试数据
          const testContent = '这是一个加密的测试文件内容。'.repeat(100);
          const encoder = new TextEncoder();
          resolve(encoder.encode(testContent));
        }
      }, 200);
    });
  }
  
  /**
   * 解密并打开文件
   */
  async decryptAndOpenFile(fileItem: FileItem): Promise<void> {
    try {
      this.downloadStatus = '解密文件中...';
      
      // 解密到临时文件
      const tempFilePath = await this.fileSecurityManager.decryptToTempFile(fileItem.encryptedPath);
      
      // 更新文件项
      fileItem.decryptedPath = tempFilePath;
      fileItem.isDecrypted = true;
      
      this.downloadStatus = '解密完成';
      
      // 打开文件
      await this.openFile(tempFilePath);
      
      prompt.showToast({
        message: '文件已解密并打开',
        duration: 2000
      });
      
    } catch (error) {
      console.error('Failed to decrypt and open file:', error);
      
      prompt.showToast({
        message: '文件解密失败',
        duration: 3000
      });
    } finally {
      this.downloadStatus = '等待下载';
    }
  }
  
  /**
   * 打开文件
   */
  private async openFile(filePath: string): Promise<void> {
    // 这里可以实现文件打开逻辑
    // 例如使用系统分享或特定应用打开
    console.log('Opening file:', filePath);
  }
  
  /**
   * 保存下载记录
   */
  private saveDownloadedFiles(): void {
    try {
      const context = getContext(this) as common.UIAbilityContext;
      const filePath = `${context.filesDir}/download_history.json`;
      
      const dataStr = JSON.stringify(this.downloadedFiles);
      const encoder = new TextEncoder();
      const data = encoder.encode(dataStr);
      
      // 实际实现中需要使用fs API保存
      console.log('Saved download history:', filePath);
    } catch (error) {
      console.error('Failed to save download history:', error);
    }
  }
  
  /**
   * 加载下载记录
   */
  private loadDownloadedFiles(): void {
    try {
      // 模拟加载已下载文件
      this.downloadedFiles = [
        {
          id: '1',
          name: '行程单_北京三日游.pdf',
          encryptedPath: '/data/encrypted_documents/行程单_北京三日游.pdf.encrypted',
          size: 204800,
          downloadTime: Date.now() - 86400000, // 1天前
          isDecrypted: false
        },
        {
          id: '2',
          name: '酒店预订确认单.docx',
          encryptedPath: '/data/encrypted_documents/酒店预订确认单.docx.encrypted',
          size: 153600,
          downloadTime: Date.now() - 172800000, // 2天前
          isDecrypted: true,
          decryptedPath: '/data/temp/decrypted_酒店预订确认单.docx'
        }
      ];
    } catch (error) {
      console.error('Failed to load download history:', error);
    }
  }
  
  build() {
    Column({ space: 20 }) {
      // 标题
      Text('安全文件下载器')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 10 })
      
      Text('下载的文件将自动加密存储,确保隐私安全')
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 30 })
      
      // 下载控制区域
      Column({ space: 15 }) {
        // 下载进度
        if (this.isDownloading) {
          Column({ space: 10 }) {
            Progress({ value: this.downloadProgress, total: 100 })
              .width('90%')
              .height(8)
              .color('#1890FF')
            
            Text(`${this.downloadStatus} ${this.downloadProgress}%`)
              .fontSize(12)
              .fontColor('#1890FF')
          }
          .width('100%')
          .margin({ bottom: 20 })
        }
        
        // 下载按钮
        Button('下载示例文件(自动加密)')
          .onClick(() => {
            this.downloadAndEncryptFile(
              'https://example.com/travel_plan.pdf',
              '旅行计划.pdf'
            );
          })
          .width('80%')
          .height(45)
          .backgroundColor('#1890FF')
          .fontColor(Color.White)
          .disabled(this.isDownloading)
      }
      .width('100%')
      .padding(20)
      .backgroundColor(Color.White)
      .border({ width: 1, color: '#F0F0F0', radius: 12 })
      
      // 已下载文件列表
      if (this.downloadedFiles.length > 0) {
        Column({ space: 15 }) {
          Text('已加密文件')
            .fontSize(18)
            .fontWeight(FontWeight.Medium)
            .fontColor('#333333')
          
          List({ space: 10 }) {
            ForEach(this.downloadedFiles, (item: FileItem) => {
              ListItem() {
                this.buildFileItem(item);
              }
            }, (item: FileItem) => item.id)
          }
          .width('100%')
          .height(400)
          .divider({ strokeWidth: 1, color: '#F0F0F0' })
        }
        .width('100%')
        .margin({ top: 20 })
      } else {
        Column() {
          Text('暂无加密文件')
            .fontSize(16)
            .fontColor('#999999')
            .margin({ top: 50 })
          
          Text('点击上方按钮下载文件,将自动加密保存')
            .fontSize(12)
            .fontColor('#999999')
            .margin({ top: 10 })
        }
        .width('100%')
        .height(200)
      }
      
      // 状态提示
      Column({ space: 8 }) {
        Text('安全提示:')
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
        
        Text('• 所有下载文件均自动AES256加密存储')
          .fontSize(12)
          .fontColor('#666666')
        
        Text('• 加密文件无法被其他应用直接访问')
          .fontSize(12)
          .fontColor('#666666')
        
        Text('• 查看文件时需要先解密到临时目录')
          .fontSize(12)
          .fontColor('#666666')
      }
      .width('90%')
      .padding(15)
      .backgroundColor('#F6FFED')
      .border({ width: 1, color: '#B7EB8F', radius: 8 })
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .padding(20)
  }
  
  /**
   * 构建文件项组件
   */
  @Builder
  buildFileItem(item: FileItem) {
    Row({ space: 15 }) {
      // 文件图标
      Column() {
        Image(item.isDecrypted ? 'app.media.icon_file_decrypted' : 'app.media.icon_file_encrypted')
          .width(24)
          .height(24)
      }
      .width(40)
      .height(40)
      .backgroundColor(item.isDecrypted ? '#52C41A20' : '#1890FF20')
      .borderRadius(20)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      
      // 文件信息
      Column({ space: 4 }) {
        Text(item.name)
          .fontSize(16)
          .fontColor('#333333')
          .fontWeight(FontWeight.Medium)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .width('80%')
        
        Row({ space: 10 }) {
          Text(this.formatFileSize(item.size))
            .fontSize(12)
            .fontColor('#999999')
          
          Text(this.formatTime(item.downloadTime))
            .fontSize(12)
            .fontColor('#999999')
        }
      }
      .layoutWeight(1)
      
      // 操作按钮
      if (item.isDecrypted) {
        Text('已解密')
          .fontSize(12)
          .fontColor('#52C41A')
      } else {
        Button('解密查看')
          .onClick(() => this.decryptAndOpenFile(item))
          .width(80)
          .height(30)
          .backgroundColor('#1890FF')
          .fontColor(Color.White)
          .fontSize(12)
      }
    }
    .width('100%')
    .padding(12)
    .backgroundColor(Color.White)
    .border({ width: 1, color: '#F0F0F0', radius: 8 })
  }
  
  /**
   * 格式化文件大小
   */
  formatFileSize(bytes: number): string {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
    return (bytes / 1048576).toFixed(1) + ' MB';
  }
  
  /**
   * 格式化时间
   */
  formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    const now = new Date();
    const diff = now.getTime() - timestamp;
    
    if (diff < 3600000) { // 1小时内
      return Math.floor(diff / 60000) + '分钟前';
    } else if (diff < 86400000) { // 24小时内
      return Math.floor(diff / 3600000) + '小时前';
    } else {
      return Math.floor(diff / 86400000) + '天前';
    }
  }
}

四、关键实现要点解析

4.1 AES加密算法选择

在HarmonyOS中,我们使用cryptoFramework模块实现AES加密。选择AES-256-ECB-PKCS7的原因:

  1. 安全性:AES-256是目前最安全的对称加密算法之一

  2. 性能:硬件加速支持,加密解密速度快

  3. 兼容性:广泛支持,便于未来可能的跨平台需求

  4. 标准化:PKCS7填充标准,保证数据完整性

4.2 文件存储策略

// 文件存储路径管理
class FileStorageManager {
  /**
   * 获取安全的文件存储路径
   */
  static getSecureStoragePath(fileName: string, category: string): string {
    const context = getContext(this) as common.UIAbilityContext;
    
    // 按文件分类存储
    const baseDirs: Record<string, string> = {
      'documents': 'encrypted_docs',
      'images': 'encrypted_imgs',
      'videos': 'encrypted_videos',
      'others': 'encrypted_others'
    };
    
    const categoryDir = baseDirs[category] || baseDirs['others'];
    return `${context.filesDir}/${categoryDir}/${this.generateUniqueName(fileName)}.encrypted`;
  }
  
  /**
   * 生成唯一文件名(防止重名覆盖)
   */
  static generateUniqueName(originalName: string): string {
    const timestamp = Date.now();
    const random = Math.random().toString(36).substring(2, 8);
    const extensionIndex = originalName.lastIndexOf('.');
    
    if (extensionIndex !== -1) {
      const name = originalName.substring(0, extensionIndex);
      const ext = originalName.substring(extensionIndex);
      return `${name}_${timestamp}_${random}${ext}`;
    }
    
    return `${originalName}_${timestamp}_${random}`;
  }
}

4.3 临时文件安全管理

临时解密文件需要特别注意安全清理:

// 临时文件管理器
class TempFileManager {
  private static tempFiles: Set<string> = new Set();
  private static cleanupInterval: number | null = null;
  
  /**
   * 创建临时文件
   */
  static async createTempFile(data: Uint8Array, extension: string): Promise<string> {
    const context = getContext(this) as common.UIAbilityContext;
    const tempDir = context.tempDir;
    const fileName = `temp_${Date.now()}_${Math.random().toString(36).substring(2)}.${extension}`;
    const filePath = `${tempDir}/${fileName}`;
    
    // 写入文件
    await this.writeFile(filePath, data);
    
    // 记录临时文件
    this.tempFiles.add(filePath);
    
    // 启动清理定时器(如果没有的话)
    if (!this.cleanupInterval) {
      this.startCleanupTimer();
    }
    
    return filePath;
  }
  
  /**
   * 清理过期临时文件
   */
  static async cleanupOldFiles(maxAge: number = 3600000): Promise<void> { // 默认1小时
    const now = Date.now();
    
    for (const filePath of this.tempFiles) {
      try {
        const stat = await fs.stat(filePath);
        const fileAge = now - stat.mtime.getTime();
        
        if (fileAge > maxAge) {
          await fs.unlink(filePath);
          this.tempFiles.delete(filePath);
          console.log(`Cleaned up old temp file: ${filePath}`);
        }
      } catch (error) {
        console.warn(`Failed to clean up temp file ${filePath}:`, error);
        this.tempFiles.delete(filePath);
      }
    }
  }
  
  /**
   * 启动清理定时器
   */
  private static startCleanupTimer(interval: number = 300000): void { // 5分钟检查一次
    this.cleanupInterval = setInterval(() => {
      this.cleanupOldFiles().catch(console.error);
    }, interval);
  }
}

五、实际应用集成

5.1 在AI旅行助手中的集成

在AI旅行助手应用中,文件加密存储功能可以这样集成:

// TravelDocumentManager.ets
@Component
export struct TravelDocumentManager {
  private fileSecurityManager = FileSecurityManager.getInstance();
  private downloadQueue: Array<DownloadTask> = [];
  private isProcessingQueue: boolean = false;
  
  /**
   * 下载旅行文档(自动加密)
   */
  async downloadTravelDocument(
    document: TravelDocument, 
    onProgress?: (progress: number) => void
  ): Promise<string> {
    try {
      // 1. 下载原始文件
      const fileData = await this.downloadDocumentData(document.url, onProgress);
      
      // 2. 加密存储
      const encryptedPath = await this.fileSecurityManager.encryptAndSaveFile(
        fileData,
        document.fileName,
        'travel_documents'
      );
      
      // 3. 记录到数据库
      await this.saveDocumentRecord({
        id: document.id,
        name: document.fileName,
        encryptedPath,
        originalUrl: document.url,
        downloadTime: Date.now(),
        isEncrypted: true
      });
      
      return encryptedPath;
      
    } catch (error) {
      console.error('Failed to download travel document:', error);
      throw new Error(`文档下载失败: ${error.message}`);
    }
  }
  
  /**
   * 查看旅行文档(自动解密)
   */
  async viewTravelDocument(documentId: string): Promise<void> {
    try {
      // 1. 查询文档记录
      const document = await this.getDocumentRecord(documentId);
      if (!document) {
        throw new Error('文档不存在');
      }
      
      // 2. 解密到临时文件
      const tempFilePath = await this.fileSecurityManager.decryptToTempFile(
        document.encryptedPath
      );
      
      // 3. 使用系统能力打开文件
      await this.openWithSystemViewer(tempFilePath);
      
      // 4. 记录查看历史
      await this.recordViewHistory(documentId);
      
    } catch (error) {
      console.error('Failed to view travel document:', error);
      throw new Error(`文档查看失败: ${error.message}`);
    }
  }
  
  /**
   * 批量下载旅行文档
   */
  async batchDownloadDocuments(
    documents: TravelDocument[], 
    onProgress?: (completed: number, total: number) => void
  ): Promise<void> {
    const total = documents.length;
    let completed = 0;
    
    for (const doc of documents) {
      try {
        await this.downloadTravelDocument(doc);
        completed++;
        
        if (onProgress) {
          onProgress(completed, total);
        }
      } catch (error) {
        console.error(`Failed to download document ${doc.fileName}:`, error);
        // 继续下载其他文档
      }
    }
  }
}

5.2 用户体验优化

为了提供更好的用户体验,可以添加以下功能:

// 用户体验增强功能
class UserExperienceEnhancer {
  /**
   * 显示加密状态指示
   */
  static showEncryptionStatus(filePath: string): void {
    const isEncrypted = filePath.endsWith('.encrypted');
    
    prompt.showToast({
      message: isEncrypted ? '🔒 文件已加密保护' : '✅ 文件可安全查看',
      duration: 2000,
      bottom: 200
    });
  }
  
  /**
   * 智能清理建议
   */
  static suggestCleanup(encryptedFiles: FileItem[]): void {
    const oldFiles = encryptedFiles.filter(file => {
      const age = Date.now() - file.downloadTime;
      return age > 30 * 24 * 3600000; // 30天前
    });
    
    if (oldFiles.length > 0) {
      AlertDialog.show({
        title: '清理建议',
        message: `发现${oldFiles.length}个加密文件超过30天未访问,是否清理以释放空间?`,
        primaryButton: {
          value: '立即清理',
          action: () => this.cleanupOldFiles(oldFiles)
        },
        secondaryButton: {
          value: '暂不清理',
          action: () => console.log('用户选择暂不清理')
        }
      });
    }
  }
}

六、安全注意事项

6.1 密钥安全管理

// 增强的密钥管理
class EnhancedKeyManager {
  private static readonly KEY_STORE_ALIAS = 'secure_file_encryption_key';
  
  /**
   * 使用系统密钥库保存密钥
   */
  static async saveKeyToKeyStore(key: Uint8Array): Promise<void> {
    try {
      const keyStore = cryptoFramework.createKeyStore();
      const symKeyGenerator = cryptoFramework.createSymKeyGenerator('AES256');
      const keyBlob: cryptoFramework.DataBlob = { data: key };
      
      const symKey = await symKeyGenerator.convertKey(keyBlob);
      await keyStore.setKey(this.KEY_STORE_ALIAS, symKey);
      
      console.info('Key saved to keystore successfully');
    } catch (error) {
      console.error('Failed to save key to keystore:', error);
      throw error;
    }
  }
  
  /**
   * 从密钥库加载密钥
   */
  static async loadKeyFromKeyStore(): Promise<cryptoFramework.SymKey | null> {
    try {
      const keyStore = cryptoFramework.createKeyStore();
      return await keyStore.getKey(this.KEY_STORE_ALIAS);
    } catch (error) {
      console.warn('Key not found in keystore:', error);
      return null;
    }
  }
}

6.2 文件完整性验证

// 文件完整性检查
class FileIntegrityChecker {
  /**
   * 计算文件哈希值
   */
  static async calculateFileHash(filePath: string): Promise<string> {
    try {
      const sha256 = cryptoFramework.createHash('SHA256');
      
      // 读取文件并计算哈希
      const file = await fs.open(filePath, fs.OpenMode.READ_ONLY);
      const buffer = new ArrayBuffer(4096);
      
      let hasMore = true;
      while (hasMore) {
        const { bytesRead } = await fs.read(file.fd, buffer);
        if (bytesRead === 0) {
          hasMore = false;
        } else {
          const data = new Uint8Array(buffer, 0, bytesRead);
          await sha256.update({ data });
        }
      }
      
      await fs.close(file.fd);
      
      const hash = await sha256.digest();
      return this.bytesToHex(hash.data);
      
    } catch (error) {
      console.error('Failed to calculate file hash:', error);
      throw error;
    }
  }
  
  /**
   * 验证文件完整性
   */
  static async verifyFileIntegrity(
    filePath: string, 
    expectedHash: string
  ): Promise<boolean> {
    try {
      const actualHash = await this.calculateFileHash(filePath);
      return actualHash === expectedHash;
    } catch (error) {
      console.error('File integrity verification failed:', error);
      return false;
    }
  }
}

七、总结与最佳实践

7.1 核心优势

通过本文的实现方案,我们获得了以下优势:

  1. 安全性提升:所有敏感文件自动加密存储,防止未授权访问

  2. 用户体验优化:加密解密过程对用户透明,无需额外操作

  3. 性能平衡:AES加密硬件加速,对应用性能影响极小

  4. 易于扩展:模块化设计,支持多种加密算法和存储策略

7.2 最佳实践建议

  1. 密钥管理

    • 使用系统密钥库存储加密密钥

    • 定期轮换密钥

    • 不同用户使用不同密钥

  2. 存储优化

    • 按文件类型分类存储

    • 定期清理临时文件

    • 实现文件分片加密(大文件)

  3. 用户体验

    • 显示加密状态指示

    • 提供批量操作功能

    • 智能清理建议

  4. 错误处理

    • 完善的异常捕获和恢复机制

    • 用户友好的错误提示

    • 操作日志记录

7.3 实际应用效果

在AI旅行助手应用中集成文件加密存储功能后:

  1. 隐私保护:用户的行程单、酒店预订等敏感信息得到有效保护

  2. 合规性:满足数据保护法规要求

  3. 用户信任:提升用户对应用安全性的信任度

  4. 竞争优势:相比不加密的应用,提供更强的安全特性

文件安全存储是现代移动应用不可或缺的功能。通过HarmonyOS提供的强大安全框架,我们可以轻松实现企业级的安全文件管理,在保护用户隐私的同时,提供流畅的用户体验。无论是旅行助手、金融应用还是企业办公软件,这套方案都能为你的应用增加重要的安全保护层。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐