Read the original article:What Is AssetStoreManager and When Should You Use It in HarmonyOS Next?
This article explores how AssetStoreManager helps secure sensitive data like tokens or passwords in HarmonyOS Next apps using AssetStoreKit, with password support and expiry.
Introduction
Have you ever wondered how to securely store a token, PIN, or encrypted configuration in a HarmonyOS Next app without relying on plaintext files or external databases?
With the rise of security-first development in 2025, Huawei’s AssetStoreKit provides a robust solution for secure data handling. In this article, we’ll build a utility class called AssetStoreManager
a singleton service layer around AssetStoreKit for managing small, sensitive key-value assets with optional password protection and expiration control.
We’ll walk through each function, show real-world examples, and explain when and why you should (or shouldn’t) use this approach in HarmonyOS Next development.
What Is AssetStoreManager?
AssetStoreManager
is a developer-defined singleton utility designed to simplify and enhance the use of Huawei’s AssetStoreKit
. It supports:
- 🔐 Secure storage and retrieval using
SECRET
encryption. - 🔑 Password-protected entries via
REQUIRE_PASSWORD_SET
. - ♾️ Persistent storage across app uninstall with
IS_PERSISTENT
. - ⏱️ Expiry-based logic for sensitive data (like short-term tokens).
- 🧱 BaseModel serialization/deserialization for JSON objects.
🧱 Feature-by-Feature Breakdown
✅ set(key, value)
Stores a string securely in the asset store using the provided key
await AssetStoreManager.getInstance().set('session_token', 'abc123');
✅ setWithPassword(key, value, password)
Stores a value that can only be retrieved by providing the matching password.
await manager.setWithPassword('secure_pin', '7856', 'myAppPassword');
✅ get(key)
Retrieves a stored value if no password is required.
const token = await manager.get('session_token');
✅ getWithPassword(key, password)
Used for reading password-protected entries.
const pin = await manager.getWithPassword('secure_pin', 'myAppPassword');
✅ remove(key)
Deletes an asset by alias.
await manager.remove('session_token');
✅ clear(keys[])
Bulk-removes assets using Promise.all
await manager.clear(['key1', 'key2', 'key3']);
✅ update(key, newValue)
Deletes and re-creates an asset (retains existing password protection if available).
await manager.update('session_token', 'newToken123');
✅ checkExists(key)
Checks whether a key is stored.
const exists = await manager.checkExists('secure_pin');
✅ saveModel(key, model)
Serializes and stores a BaseModel
instance as JSON.
await manager.saveModel('profile', new UserModel(...));
✅ saveModelWithExpiry(key, model, duration)
Stores a model with an expiration time.
await manager.saveModelWithExpiry('otp', otpModel, Duration.seconds(120));
✅ getModelWithExpiry(key, fromJson)
Retrieves and validates the model, checking expiry before returning.
🧠 Pro Tip: AssetStoreKit is great for secrets, not bulk data.
📦 Full Code Repository
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;
🧾 Conclusion
In today’s mobile-first world, secure local storage is more than a nice-to-have — it’s a necessity. AssetStoreManager
helps developers using HarmonyOS Next implement consistent, encrypted, and maintainable data storage for small but critical pieces of information.
This is especially valuable for:
- Authentication tokens
- One-time passwords (OTP)
- User preferences
- Access control flags
Before using it, evaluate whether the data truly requires encryption and persistence. For larger or frequently changing data, consider other storage kits.
Let us know how you’ve used AssetStoreKit — or what you’d like to see added!
Top comments (0)