0
0
Lập trình
TT

AssetStoreManager là gì và khi nào nên sử dụng trong HarmonyOS Next?

Đăng vào 7 tháng trước

• 10 phút đọc

Chủ đề:

KungFuTech

Giới thiệu

Bạn có bao giờ tự hỏi làm thế nào để lưu trữ an toàn một token, mã PIN hoặc cấu hình được mã hóa trong ứng dụng HarmonyOS Next mà không phải dựa vào các tệp văn bản hay cơ sở dữ liệu bên ngoài? Trong bối cảnh phát triển bảo mật ngày càng gia tăng vào năm 2025, AssetStoreKit của Huawei cung cấp một giải pháp mạnh mẽ cho việc xử lý dữ liệu an toàn.

Trong bài viết này, chúng ta sẽ xây dựng một lớp tiện ích có tên AssetStoreManager, một lớp dịch vụ singleton xung quanh AssetStoreKit để quản lý các tài sản nhạy cảm theo cặp khóa-giá trị với tùy chọn bảo vệ bằng mật khẩu và kiểm soát thời gian hết hạn.

Chúng ta sẽ đi qua từng chức năng, minh họa bằng các ví dụ thực tế, và giải thích khi nào và tại sao bạn nên (hoặc không nên) sử dụng cách tiếp cận này trong phát triển HarmonyOS Next.

AssetStoreManager là gì?

AssetStoreManager là một tiện ích singleton do lập trình viên định nghĩa, được thiết kế để đơn giản hóa và tăng cường việc sử dụng AssetStoreKit của Huawei. Nó hỗ trợ:

  • 🔐 Lưu trữ và truy xuất an toàn bằng cách sử dụng mã hóa SECRET.
  • 🔑 Các mục được bảo vệ bằng mật khẩu thông qua REQUIRE_PASSWORD_SET.
  • ♾️ Lưu trữ bền vững qua việc gỡ cài đặt ứng dụng với IS_PERSISTENT.
  • ⏱️ Logic dựa trên thời gian hết hạn cho dữ liệu nhạy cảm (như token ngắn hạn).
  • 🧱 Serialization/deserialization của BaseModel cho các đối tượng JSON.

Phân tích từng tính năng

set(key, value)

Lưu trữ một chuỗi một cách an toàn trong cửa hàng tài sản với khóa đã cung cấp.

javascript Copy
await AssetStoreManager.getInstance().set('session_token', 'abc123');

setWithPassword(key, value, password)

Lưu trữ một giá trị chỉ có thể được truy xuất bằng cách cung cấp mật khẩu phù hợp.

javascript Copy
await manager.setWithPassword('secure_pin', '7856', 'myAppPassword');

get(key)

Truy xuất một giá trị đã lưu nếu không yêu cầu mật khẩu.

javascript Copy
const token = await manager.get('session_token');

getWithPassword(key, password)

Được sử dụng để đọc các mục được bảo vệ bằng mật khẩu.

javascript Copy
const pin = await manager.getWithPassword('secure_pin', 'myAppPassword');

remove(key)

Xóa một tài sản theo bí danh.

javascript Copy
await manager.remove('session_token');

clear(keys[])

Xóa hàng loạt các tài sản bằng cách sử dụng Promise.all

javascript Copy
await manager.clear(['key1', 'key2', 'key3']);

update(key, newValue)

Xóa và tạo lại một tài sản (giữ nguyên bảo vệ mật khẩu hiện có nếu có).

javascript Copy
await manager.update('session_token', 'newToken123');

checkExists(key)

Kiểm tra xem một khóa có được lưu trữ hay không.

javascript Copy
const exists = await manager.checkExists('secure_pin');

saveModel(key, model)

Serialization và lưu trữ một phiên bản BaseModel dưới dạng JSON.

javascript Copy
await manager.saveModel('profile', new UserModel(...));

saveModelWithExpiry(key, model, duration)

Lưu trữ một mô hình với thời gian hết hạn.

javascript Copy
await manager.saveModelWithExpiry('otp', otpModel, Duration.seconds(120));

getModelWithExpiry(key, fromJson)

Truy xuất và xác thực mô hình, kiểm tra thời gian hết hạn trước khi trả về.

Mẹo chuyên nghiệp: AssetStoreKit rất tốt cho các bí mật, không phải cho dữ liệu khối.

Mã nguồn đầy đủ

javascript Copy
import { asset } from '@kit.AssetStoreKit';
import { util } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { BaseModel } from '../base/BaseModel';
import { Duration } from '../base/DurationModel';
import { LogManager } from '../log/LogManager';

function stringToArray(str: string): Uint8Array {
  const encoder = new util.TextEncoder();
  return encoder.encodeInto(str);
}

function arrayToString(arr: Uint8Array): string {
  const decoder = new util.TextDecoder('utf-8', { ignoreBOM: true });
  return decoder.decode(arr);
}

interface AssetDataExpiryModel<TJson> {
  data: TJson;
  expireAt: number;
}

interface AssetStoreInterface {
  persistent?: boolean;
  accessibility?: number;
}

export class AssetStoreManager {
  private static instance: AssetStoreManager;
  private isPersistent: boolean = true;
  private accessibility: number = asset.Accessibility.DEVICE_FIRST_UNLOCKED;
  private initialized = false;
  private static TAG = 'AssetStoreManager'

  private constructor() {}

  static getInstance(): AssetStoreManager {
    if (!AssetStoreManager.instance) {
      AssetStoreManager.instance = new AssetStoreManager();
    }
    return AssetStoreManager.instance;
  }

  public init(options?: AssetStoreInterface): void {
    if (this.initialized){
     return
    }

    if (options?.persistent) {
      this.isPersistent = options.persistent ?? true;
    }
    if (typeof options?.accessibility === 'number') {
      this.accessibility = options.accessibility;
    }

    this.initialized = true;
    LogManager.info(AssetStoreManager.TAG,`AssetStoreManager initialized. persistent=${!!options?.persistent}, accessibility=${this.accessibility}`)
  }

  async set(key: string, value: string): Promise<void> {
    const attr: asset.AssetMap = new Map();
    attr.set(asset.Tag.ALIAS, stringToArray(key));
    attr.set(asset.Tag.SECRET, stringToArray(value));
    attr.set(asset.Tag.ACCESSIBILITY, this.accessibility);
    attr.set(asset.Tag.IS_PERSISTENT, this.isPersistent);
    attr.set(asset.Tag.DATA_LABEL_NORMAL_1, stringToArray('label-' + key));

    try {
      await asset.add(attr);
      LogManager.info(AssetStoreManager.TAG,`Asset added for key: ${key}`)
    } catch (err) {
      const e = err as BusinessError;
      LogManager.error(AssetStoreManager.TAG,`Asset add failed. Code: ${e.code}, message: ${e.message}`)
    }
  }

  async get(key: string): Promise<string | null> {
    const queryMap: asset.AssetMap = new Map();
    queryMap.set(asset.Tag.ALIAS, stringToArray(key));
    queryMap.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);

    try {
      const results = await asset.query(queryMap);
      if (!results || results.length === 0){
        return null
      }
      const secret = results[0].get(asset.Tag.SECRET) as Uint8Array | undefined;
      return secret ? arrayToString(secret) : null;
    } catch (err) {
      const e = err as BusinessError;
      LogManager.error(AssetStoreManager.TAG,`Asset get failed. Code: ${e.code}, message: ${e.message}`)
      return null;
    }
  }

  async setWithPassword(key: string, value: string): Promise<void> {
    const attr: asset.AssetMap = new Map();
    attr.set(asset.Tag.ALIAS, stringToArray(key));
    attr.set(asset.Tag.SECRET, stringToArray(value));
    attr.set(asset.Tag.REQUIRE_PASSWORD_SET, true);
    attr.set(asset.Tag.ACCESSIBILITY, this.accessibility);
    attr.set(asset.Tag.IS_PERSISTENT, this.isPersistent);
    attr.set(asset.Tag.DATA_LABEL_NORMAL_1, stringToArray('label-' + key));

    try {
      await asset.add(attr);
    } catch (err) {
      LogManager.error(AssetStoreManager.TAG,`Asset add with password failed for key: ${key}`)
    }
  }

  async getWithPassword(key: string): Promise<string | null> {
    const queryMap: asset.AssetMap = new Map();
    queryMap.set(asset.Tag.ALIAS, stringToArray(key));
    queryMap.set(asset.Tag.REQUIRE_PASSWORD_SET, true);
    queryMap.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);

    try {
      const results = await asset.query(queryMap);
      if (!results || results.length === 0){
        return null
      }
      const secret = results[0].get(asset.Tag.SECRET) as Uint8Array | undefined;
      return secret ? arrayToString(secret) : null;
    } catch (err) {
      LogManager.error(AssetStoreManager.TAG,`Asset get with password failed for key: ${key}`)
      return null;
    }
  }

  async remove(key: string): Promise<void> {
    const removeMap: asset.AssetMap = new Map();
    removeMap.set(asset.Tag.ALIAS, stringToArray(key));

    try {
      await asset.remove(removeMap);
    } catch (err) {
      LogManager.error(AssetStoreManager.TAG,`Asset remove failed for key: ${key}`)
    }
  }

  async clear(keys: string[]): Promise<void> {
    await Promise.all(keys.map(key => this.remove(key)));
  }

  async update(key: string, newValue: string): Promise<void> {
    const queryMap: asset.AssetMap = new Map();
    queryMap.set(asset.Tag.ALIAS, stringToArray(key));
    queryMap.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);

    try {
      const results = await asset.query(queryMap);
      if (!results || results.length === 0){
        return
      }

      const existing = results[0];
      await asset.remove(queryMap);

      const attr: asset.AssetMap = new Map();
      attr.set(asset.Tag.ALIAS, stringToArray(key));
      attr.set(asset.Tag.SECRET, stringToArray(newValue));
      attr.set(asset.Tag.ACCESSIBILITY, this.accessibility);
      attr.set(asset.Tag.IS_PERSISTENT, this.isPersistent);
      attr.set(asset.Tag.DATA_LABEL_NORMAL_1, stringToArray('label-' + key));

      const pwd = existing.get(asset.Tag.REQUIRE_PASSWORD_SET);
      if (pwd){
        attr.set(asset.Tag.REQUIRE_PASSWORD_SET, pwd);
      }

      await asset.add(attr);
    } catch (err) {
      LogManager.error(AssetStoreManager.TAG,`Asset update failed for key: ${key}`)
    }
  }

  async checkExists(key: string): Promise<boolean> {
    const queryMap: asset.AssetMap = new Map();
    queryMap.set(asset.Tag.ALIAS, stringToArray(key));
    queryMap.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ATTRIBUTES);

    try {
      const result = await asset.query(queryMap);
      return result.length > 0 && result[0].has(asset.Tag.SECRET);
    } catch (err) {
      LogManager.error(AssetStoreManager.TAG, `Asset existence check failed for key: ${key}`)
      return false;
    }
  }

  async saveModel<T extends BaseModel>(key: string, model: T): Promise<void> {
    await this.set(key, JSON.stringify(model.toJson()));
  }

  async saveModelWithExpiry<T extends BaseModel>(key: string, model: T, duration: Duration): Promise<void> {

    const ms = duration.inMilliseconds();
    if (ms <= 0){
      throw new Error('Invalid duration for expiry');
    }

    const wrapper: AssetDataExpiryModel<object> = {
      data: model.toJson(),
      expireAt: Date.now() + ms
    };

    try {
      hilog.error(0,'asset','try catch')
      await this.remove(key)
      await this.set(key, JSON.stringify(wrapper));
    } catch (e) {
      LogManager.error(AssetStoreManager.TAG, `save model with expiry error: ${e}`)
    }
  }

  async updateModelWithExpiry<T extends BaseModel>(key: string, model: T, duration: Duration): Promise<void> {
    const ms = duration.inMilliseconds();
    if (ms <= 0){
      throw new Error('Invalid duration for expiry');
    }

    const wrapper: AssetDataExpiryModel<object> = {
      data: model.toJson(),
      expireAt: Date.now() + ms
    };
    await this.update(key, JSON.stringify(wrapper));
  }

  async getModelWithExpiry<TModel, TJson>(key: string, fromJson: (json: TJson) => TModel): Promise<TModel | null> {
    const value = await this.get(key);
    if (!value){
      return null
    }

    try {
      const parsed: AssetDataExpiryModel<TJson> = JSON.parse(value);
      if (Date.now() > parsed.expireAt) {
        await this.remove(key);
        return null;
      }

      return fromJson(parsed.data);
    } catch (e) {
      LogManager.error(AssetStoreManager.TAG, `getModelWithExpiry parse error for key: ${key} ${e}`)
      return null;
    }
  }

  async getJson<TModel, TJson>(key: string, fromJson: (json: TJson) => TModel): Promise<TModel | null> {
    const value = await this.get(key);
    if (!value){
      return null
    }

    try {
      const parsed: TJson = JSON.parse(value);
      return fromJson(parsed);
    } catch (e) {
      LogManager.error(AssetStoreManager.TAG, `getJson parse error for key: ${key}`)
      return null;
    }
  }

  async getModel<TModel, TJson>(key: string, fromJson: (json: TJson) => TModel): Promise<TModel | null> {
    return this.getJson<TModel, TJson>(key, fromJson);
  }
}

export default AssetStoreManager;

Kết luận

Trong thế giới di động ngày nay, lưu trữ cục bộ an toàn không chỉ là một lựa chọn — nó là một yêu cầu thiết yếu. AssetStoreManager giúp các nhà phát triển sử dụng HarmonyOS Next triển khai lưu trữ dữ liệu nhất quán, mã hóa và dễ bảo trì cho các thông tin nhỏ nhưng quan trọng.

Điều này đặc biệt có giá trị cho:

  • Token xác thực
  • Mã OTP (One-time password)
  • Tuỳ chọn người dùng
  • Cờ kiểm soát truy cập

Trước khi sử dụng, hãy đánh giá xem dữ liệu có thực sự cần mã hóa và tính bền vững hay không. Đối với dữ liệu lớn hoặc thay đổi thường xuyên, hãy cân nhắc các bộ công cụ lưu trữ khác.

Hãy cho chúng tôi biết bạn đã sử dụng AssetStoreKit như thế nào — hoặc những gì bạn muốn thấy thêm!

Tài liệu tham khảo

Asset Store Kit

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào