鸿蒙原生密码保险箱 PasswordVault 应用开发实战

基于 ArkTS + SQLite + AES-256-GCM 构建的个人密码安全管家


一、项目概览

PasswordVault 是一款运行在鸿蒙(HarmonyOS)原生平台上的个人密码保险箱应用。它使用纯 ArkTS 声明式 UI 框架构建界面,借助 @ohos.data.relationalStore(SQLite)实现本地数据持久化,通过 @ohos.security.cryptoFramework 提供的 AES-256-GCM 加密算法保护用户密码,并集成了系统剪贴板实现一键复制功能。

技术栈一览:

技术组件 用途
ArkTS + @State 响应式框架 构建声明式 UI
@ohos.data.relationalStore SQLite 关系型数据库
@ohos.security.cryptoFramework AES-256-GCM 加解密
@ohos.pasteboard 系统剪贴板服务
@ohos.util TextEncoder/TextDecoder 编解码
API Version 24+ 鸿蒙原生 API

架构层次:

表示层 (PasswordVault / PasswordEditDialog / PasswordDetailDialog)
       ↓
业务逻辑层 (CRUD 编排 / 搜索筛选 / 剪贴板)
       ↓
加密层 (CryptoUtil — AES-256-GCM)
       ↓
数据层 (PasswordDatabase — SQLite)

以"新增密码"为例的完整数据流:用户填写表单 → 点击确认 → addPassword() 调用 vaultDB.insert() → 内部先 cryptoUtil.encrypt() 加密明文 → 加密密文写入 SQLite → 成功后重新 loadData() → 逐条 cryptoUtil.decrypt() 解密 → @State displayList 更新,UI 自动渲染。


二、数据模型设计

2.1 分类定义

应用预定义了 7 个密码分类,每个含名称、Emoji 图标和主题色:

分类 图标 主题色
社交媒体 💬 #8E74FF
电子邮件 ✉️ #007AFF
金融财务 💰 #34C759
购物消费 🛒 #FF9500
工作办公 💼 #FF3B30
娱乐影音 🎵 #FF2D55
其他 📁 #8E8E93

2.2 三层数据模型

PasswordItem(存储模型)——对应数据库表记录:

interface PasswordItem {
  id: number;                // 自增主键
  category: string;          // 分类
  appName: string;           // 应用名称
  username: string;          // 账户名
  encryptedPassword: string; // AES 加密后的 Base64 密文
  notes: string;             // 备注
  createTime: string;        // 创建时间 ISO 8601
  updateTime: string;        // 更新时间 ISO 8601
}

InsertPasswordItem(输入模型)——新增/修改时使用,包含明文密码:

interface InsertPasswordItem {
  category: string;
  appName: string;
  username: string;
  password: string;  // 明文,传入后加密存储
  notes: string;
}

PasswordDisplayItem(展示模型)——界面渲染使用,增加 UI 状态:

interface PasswordDisplayItem {
  // ...同 PasswordItem 字段
  password: string;       // 解密后的明文
  showPassword: boolean;  // 控制密码明文显隐
}

三、数据库实现(PasswordDatabase)

3.1 初始化

使用单例模式(const vaultDB = new PasswordDatabase()),确保全局只有一个数据库连接:

async init(context: Context): Promise<void> {
  if (this.rdbStore) return;
  const config: relationalStore.StoreConfig = {
    name: 'password_vault.db',
    securityLevel: relationalStore.SecurityLevel.S1, // 敏感信息等级
  };
  this.rdbStore = await relationalStore.getRdbStore(context, config);
  await this.createTable();
}

SecurityLevel.S1 告知系统该数据库包含个人敏感信息,系统会启用更严格的数据保护。

3.2 表结构

CREATE TABLE IF NOT EXISTS passwords (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  category TEXT NOT NULL,
  appName TEXT NOT NULL,
  username TEXT NOT NULL,
  encryptedPassword TEXT NOT NULL,
  notes TEXT DEFAULT '',
  createTime TEXT DEFAULT '',
  updateTime TEXT DEFAULT ''
)

3.3 CRUD 操作

插入——三步走:校验必填字段 → AES 加密密码 → 写入数据库:

async insert(item: InsertPasswordItem): Promise<number> {
  this.validateInsert(item);  // 校验 appName/username/password/category
  const encryptedPwd = await cryptoUtil.encrypt(item.password);
  const now = new Date().toISOString();
  const rowId = await this.rdbStore.insert(TABLE_NAME, {
    category: item.category,
    appName: item.appName.trim(),
    username: item.username.trim(),
    encryptedPassword: encryptedPwd,
    notes: item.notes.trim(),
    createTime: now,
    updateTime: now,
  });
  return rowId;
}

查询——三种模式:全量 queryAll()(按 updateTime 降序)、按分类 queryByCategory(category)(精确匹配)、模糊搜索 queryBySearch(keyword)(对 appNameusernamenotes 三字段 LIKE 匹配):

async queryBySearch(keyword: string): Promise<PasswordItem[]> {
  const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
  predicates.like('appName', `%${keyword}%`).or()
    .like('username', `%${keyword}%`).or()
    .like('notes', `%${keyword}%`);
  predicates.orderByDesc('updateTime');
  // 执行查询...
}

更新——先加密新密码,再按 id 精确更新,保留 createTime 不变:

async update(id: number, item: InsertPasswordItem): Promise<void> {
  const encryptedPwd = await cryptoUtil.encrypt(item.password);
  const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
  predicates.equalTo('id', id);
  await this.rdbStore.update({ ...value, updateTime: new Date().toISOString() }, predicates);
}

删除——按主键删除,简单直接。


四、AES-256-GCM 加密实现(CryptoUtil)

4.1 算法选型

特性 说明
密钥长度 256 位 业界最高强度对称加密
GCM 认证加密模式 同时提供机密性和完整性校验
12 字节随机 IV 相同明文每次产生不同密文
鸿蒙原生支持 系统级安全组件,经过安全审计

4.2 密钥初始化

当前版本使用固定演示密钥(生产环境应使用生物识别/主密码派生):

// 生产环境应使用 PBKDF2/Argon2 从用户主密码派生
const AES_KEY_B64 = 'dGVzdC1rZXktMzItYnl0ZXMtZm9yLWRlbW8tYWFhYQ==';

private async initKey(): Promise<void> {
  const keyDataBin = this.base64ToBytes(AES_KEY_B64);
  const symKeyGenerator = cryptoFramework.createSymKeyGenerator('AES256');
  this.keyBlob = await symKeyGenerator.convertKey({ data: keyDataBin });
}

4.3 加密流程

明文 → AES-256-GCM 加密 → IV(12B) + 密文(N B) + AuthTag(16B) → Base64 编码 → 存储字符串

核心实现:

async encrypt(plainText: string): Promise<string> {
  await this.assertKeyReady();
  const ivData = this.generateRandomBytes(12);
  const authTagBuf = new Uint8Array(16);
  const gcmParams: GcmParamsSpec = {
    iv: { data: ivData },
    aad: { data: new Uint8Array(0) },
    authTag: { data: authTagBuf },
    algName: 'GCM',
  };
  const cipher = cryptoFramework.createCipher('AES256|GCM|PKCS7');
  await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, this.keyBlob!, gcmParams);
  const cipherBlob = await cipher.update({ data: this.stringToBytes(plainText) });
  const finalBlob = await cipher.doFinal(null);
  // doFinal 后 gcmParams.authTag 被框架填充实际认证标签
  const combined = this.concatUint8Array(
    this.concatUint8Array(ivData, cipherBlob.data, finalBlob.data),
    gcmParams.authTag.data,
  );
  return this.bytesToBase64(combined);
}

4.4 解密流程

解密是加密的逆过程:Base64 解码 → 拆分 IV(前12B) + 认证标签(后16B) + 密文(中间) → 构造 GCM 参数 → 解密。

若数据被篡改导致认证标签校验失败,框架会抛出异常。解密失败时返回 '*** 解密失败 ***' 而非抛异常,避免单条数据问题导致整个列表无法渲染。

4.5 关键工具函数

因 ArkTS 无浏览器标准 atob/btoa,手动实现 Base64 编解码。配合 util.TextEncoderstringToBytes)和 util.TextDecoderbytesToString)完成文本与字节数组的互转。


五、UI 组件详解

5.1 PasswordVault 主页面(@Entry @Component

作为应用入口页面(EntryAbility.etsloadContent('pages/PasswordVault')),管理全局状态:

struct PasswordVault {
  @State displayList: PasswordDisplayItem[] = [];
  @State isLoading: boolean = true;
  @State searchKeyword: string = '';
  @State selectedCategory: string = '';   // '' = 全部
  @State showCategoryPicker: boolean = false;
  private rawItems: PasswordItem[] = [];   // 原始数据缓存
}

页面布局(自上而下):

  1. 顶部导航栏:标题"密码保险箱" + 新增按钮
  2. 搜索栏:关键词实时搜索
  3. 分类筛选栏:横向滚动的标签选择器
  4. 密码列表:List + ForEach 渲染密码卡片
  5. 底部统计:总条目数

生命周期与数据加载:

aboutToAppear(): void { this.initDatabase(); }

private async initDatabase(): Promise<void> {
  const context = getContext(this);
  await vaultDB.init(context);
  await this.loadData();
  this.isLoading = false;
}

private async loadData(): Promise<void> {
  let items: PasswordItem[];
  if (this.selectedCategory && this.selectedCategory !== '全部')
    items = await vaultDB.queryByCategory(this.selectedCategory);
  else if (this.searchKeyword.trim())
    items = await vaultDB.queryBySearch(this.searchKeyword.trim());
  else
    items = await vaultDB.queryAll();
  this.rawItems = items;
  await this.rebuildDisplayList(items);
}

逐条解密并构建展示列表:

private async rebuildDisplayList(items: PasswordItem[]): Promise<void> {
  const displayList: PasswordDisplayItem[] = [];
  for (const item of items) {
    const decrypted = await cryptoUtil.decrypt(item.encryptedPassword);
    displayList.push({
      id: item.id, category: item.category, appName: item.appName,
      username: item.username, password: decrypted, notes: item.notes,
      createTime: item.createTime, updateTime: item.updateTime, showPassword: false,
    });
  }
  this.displayList = displayList;
}

5.2 PasswordEditDialog(新增/编辑弹窗)

@CustomDialog 组件,通过 editItem 参数区分新增/编辑模式:

  • 新增模式editItem = undefined):所有字段为空
  • 编辑模式editItem = PasswordDisplayItem):自动填充已有数据

表单字段包含应用名称、账户名、密码、分类选择、备注。密码输入支持显示/隐藏切换(InputType.Password / InputType.Normal 动态切换)。

实时校验反馈: 每个输入框关联 @State xxxError 错误状态,用户输入时自动清除错误;提交时若存在空字段,对应位置显示红色错误提示。

弹窗使用 DialogAlignment.Bottom 配置为底部弹出样式,内容区域由 Scroll 包裹以支持滚动。

5.3 PasswordDetailDialog(详情弹窗)

展示密码完整信息,默认隐藏密码明文(显示 ••••••••••••),支持:

  • 复制密码:调用 pasteboard.getSystemPasteboard().setData(data) 写入系统剪贴板
  • 显示/隐藏:点击眼睛图标 👁️/🙈 切换明文
  • 编辑:关闭详情,打开编辑弹窗并预填数据
  • 删除AlertDialog 二次确认,红色警示"此操作不可恢复"

5.4 密码卡片(buildPasswordCard)

每张卡片采用白色圆角 + 细阴影的 Material 风格:

┌──────────────────────────────────┐
│ 💬 社交媒体   微信             📋 │
│ 👤 user@example.com           👁️ │
│ 🔑 ••••••••••••                  │
│ 06月05日 10:48                   │
└──────────────────────────────────┘

点击卡片主体 → 打开详情;点击复制按钮 → 复制密码(阻止冒泡);点击显隐按钮 → item.showPassword = !item.showPassword; this.displayList = [...this.displayList](展开运算符触发 @State 更新)。


六、搜索与分类筛选

搜索和分类筛选共用 loadData() 方法,优先级:分类筛选 > 关键词搜索 > 全量查询

每个分类标签显示 Emoji + 名称,选中态使用主题色高亮:

Text(`${cat.icon} ${cat.name}`)
  .fontColor(this.selectedCategory === cat.name ? cat.color : '#8E8E93')
  .backgroundColor(this.selectedCategory === cat.name ? cat.color + '20' : '#F2F2F7')

交互细节:再次点击已选分类可取消筛选;筛选变化时显示 loading 状态。


七、安全设计

7.1 当前措施

措施 实现
密码加密存储 AES-256-GCM 加密后写入数据库
认证加密 GCM 模式同时保机密性和完整性
随机 IV 12 字节随机数,相同明文每次密文不同
敏感信息分级 数据库 SecurityLevel.S1
删除确认 AlertDialog 二次确认
密码默认隐藏 列表和详情页均以密文显示

7.2 生产环境改进建议

  1. 主密码验证:应用启动时验证用户主密码或生物识别
  2. 密钥派生:使用 PBKDF2/Argon2 从主密码派生 AES 密钥
  3. HUKS 密钥存储:利用鸿蒙通用密钥库,密钥不出安全硬件
  4. 自动锁定:后台切换超时后自动锁定
  5. 剪贴板自动清除:复制密码后定时清除剪贴板内容

八、开发环境与运行配置

应用包名 com.example.demo0528,版本 1.0.0,仅支持 Phone 设备。页面路由注册在 main_pages.json

{ "src": ["pages/Index", "pages/TodoList", "pages/PasswordVault"] }

运行要求:API 24+、HarmonyOS 真机或模拟器、DevEco Studio 开发工具。


九、总结与展望

PasswordVault 展示了鸿蒙原生开发的完整技术实践:

  • ArkTS 响应式 UI@State + @Component + @CustomDialog 构建声明式界面
  • SQLite 数据持久化relationalStore 实现本地 CRUD 和条件检索
  • AES-256-GCM 加密cryptoFramework 实现工业级密码保护
  • 系统服务集成pasteboard 实现剪贴板复制
  • 安全设计:多层次的存储加密和操作防护

可扩展方向

  • 数据加密备份与恢复
  • 强密码生成器
  • 密码健康检查(弱密码/重复密码检测)
  • 鸿蒙 AutoFill 自动填充
  • 端到端加密云同步
  • 深色模式适配

本文基于 PasswordVault.ets 源码(1563 行)撰写,完整源码位于 entry/src/main/ets/pages/PasswordVault.ets

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐