Read the original article:Authenticated Access to Secrets with Asset Store Service
Implementing User-Authenticated Access to Secrets with Asset Store Service
Requirement Description
Design and implement a secure storage flow where access to a secret (such as an API token) requires explicit user authentication (PIN, face, or fingerprint) at read time (and optionally at update/remove). The solution must:
- Store the secret encrypted in the Asset Store Service (Secure Kit).
- Enforce device lock and user authentication before returning plaintext.
- Support the full CRUD lifecycle (save, read, update, remove).
- Provide robust error handling and user-friendly messages.
Background Knowledge
-
Module:
@kit.AssetStoreKit (asset) -
Data model:
AssetMap (Map<Tag, Value>)
Relevant tags:
- ALIAS (bytes): Unique identifier for the secret.
- SECRET (bytes): The plaintext secret to store.
-
ACCESSIBILITY (number): Access policy (e.g.,
DEVICE_FIRST_UNLOCKED,DEVICE_UNLOCKED). -
AUTH_TYPE (number): Required authentication type (e.g.,
ANY,NONE). - AUTH_VALIDITY_PERIOD (number): Grace window (ms) after a successful authentication.
- REQUIRE_PASSWORD_SET (bool): Enforce that a device lock (PIN/password) is configured.
Authenticated query flow:
Implementation Steps
- Choose an alias for the secret (e.g.,
secure_api_token_v1). - Save the secret with authentication requirements:
ACCESSIBILITY=DEVICE_UNLOCKEDREQUIRE_PASSWORD_SET=trueAUTH_TYPE=ANY- Optional:
AUTH_VALIDITY_PERIOD=30–120s
- Read with user authentication:
- Call
asset.preQuery()→ receive a challenge. - Perform system authentication (PIN/biometric) bound to that challenge.
- Call
asset.query()to fetch the data. - Call
asset.postQuery()with the challenge to close the flow.
- Call
- Update and Remove follow the same pattern if user auth is required.
- Implement robust error handling and map technical codes to clear UX messages.
Code Snippet / Configuration
assetAuthService.ets
import { asset } from '@kit.AssetStoreKit';
import util from '@ohos.util';
const enc = new util.TextEncoder();
const dec = new util.TextDecoder();
function toBytes(s: string): Uint8Array {
return enc.encode(s);
}
function fromBytes(b?: Uint8Array): string {
return b ? dec.decode(b) : '';
}
export const TOKEN_ALIAS = 'secure_api_token_v1';
// 1) Save secret with authentication policy
export async function saveSecretWithPolicy(secret: string): Promise<void> {
const attrs: asset.AssetMap = new Map();
attrs.set(asset.Tag.ALIAS, toBytes(TOKEN_ALIAS));
attrs.set(asset.Tag.SECRET, toBytes(secret));
attrs.set(asset.Tag.ACCESSIBILITY, asset.Accessibility.DEVICE_UNLOCKED);
attrs.set(asset.Tag.REQUIRE_PASSWORD_SET, true);
attrs.set(asset.Tag.AUTH_TYPE, asset.AuthType.ANY);
attrs.set(asset.Tag.AUTH_VALIDITY_PERIOD, 60_000); // 60s grace window
try {
await asset.add(attrs);
} catch (e) {
const code = (e as { code?: number | string }).code;
if (String(code) === '24000003') {
const upd: asset.AssetMap = new Map();
upd.set(asset.Tag.ALIAS, toBytes(TOKEN_ALIAS));
upd.set(asset.Tag.SECRET, toBytes(secret));
await asset.update(upd);
return;
}
throw e;
}
}
// 2) Authenticated read
export async function readSecretWithUserAuth(
doUserAuth: (challenge: Uint8Array) => Promise<Uint8Array>
): Promise<string | null> {
const query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, toBytes(TOKEN_ALIAS));
const challenge: Uint8Array = await asset.preQuery(query);
await doUserAuth(challenge);
const results: Array<asset.AssetMap> = await asset.query(query);
const handle: asset.AssetMap = new Map();
handle.set(asset.Tag.AUTH_CHALLENGE, challenge);
await asset.postQuery(handle);
if (!results.length) return null;
const secretBytes = results[0].get(asset.Tag.SECRET) as Uint8Array | undefined;
return fromBytes(secretBytes);
}
// 3) Authenticated update
export async function updateSecretWithUserAuth(
newValue: string,
doUserAuth: (challenge: Uint8Array) => Promise<Uint8Array>
): Promise<void> {
const query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, toBytes(TOKEN_ALIAS));
const challenge: Uint8Array = await asset.preQuery(query);
await doUserAuth(challenge);
const upd: asset.AssetMap = new Map();
upd.set(asset.Tag.ALIAS, toBytes(TOKEN_ALIAS));
upd.set(asset.Tag.SECRET, toBytes(newValue));
await asset.update(upd);
const handle: asset.AssetMap = new Map();
handle.set(asset.Tag.AUTH_CHALLENGE, challenge);
await asset.postQuery(handle);
}
// 4) Authenticated remove
export async function removeSecretWithUserAuth(
doUserAuth: (challenge: Uint8Array) => Promise<Uint8Array>
): Promise<void> {
const query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, toBytes(TOKEN_ALIAS));
const challenge: Uint8Array = await asset.preQuery(query);
await doUserAuth(challenge);
await asset.remove(query);
const handle: asset.AssetMap = new Map();
handle.set(asset.Tag.AUTH_CHALLENGE, challenge);
await asset.postQuery(handle);
}
AuthPage.ets (Wearable UI with PIN keypad + Success screen)
import router from '@ohos.router';
import { readSecretWithUserAuth } from '../services/assetAuthService';
@Entry
@Component
struct AuthPage {
@State pinInput: string = '';
@State status: string = 'Enter PIN to unlock';
private async onUnlock(): Promise<void> {
try {
const val = await readSecretWithUserAuth(async (challenge) => {
// Here, PIN entry should be verified against system IAM
// For demo, we just return empty
return new Uint8Array(0);
});
this.status = `Unlocked: ${val ?? ''}`;
router.pushUrl({ url: 'pages/AuthSuccessPage', params: { token: val ?? '' } });
} catch (e) {
this.status = 'Auth failed';
}
}
build() {
Column() {
Text(this.status).fontSize(10).margin({ bottom: 6 }).fontColor('#fff')
// PIN entry display
Row() {
Text('*'.repeat(this.pinInput.length)).fontSize(14).fontColor('#4CAF50')
}.margin({ bottom: 8 })
// Keypad grid
GridRow() {
['1','2','3','4','5','6','7','8','9','Delete','0','Unlock'].forEach((label) => {
Button(label)
.height(28).width(48)
.margin({ top: 4, bottom: 4, left: 2, right: 2 })
.fontSize(10)
.onClick(() => {
if (label === 'Delete') {
this.pinInput = this.pinInput.slice(0, -1);
} else if (label === 'Unlock') {
this.onUnlock();
} else {
this.pinInput += label;
}
})
})
}
}
.width('100%').height('100%')
.backgroundColor('#000')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}
AuthSuccessPage.ets (After unlock)
import router from '@ohos.router';
@Entry
@Component
struct AuthSuccessPage {
@State token: string = '';
build() {
Column() {
Image($r('app.media.ic_success')).width(36).height(36).margin({ bottom: 8 })
Text('Access Granted')
.fontSize(12)
.fontColor('#4CAF50')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 6 })
Text(`Token: ${this.token}`)
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ bottom: 10 })
Button('Done')
.width('60%').height(24).fontSize(9)
.borderRadius(10).backgroundColor('#4CAF50')
.onClick(() => router.back())
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.width('100%').height('100%')
.backgroundColor('#000')
}
}
Test Results
- No lock screen set: Reads fail until user sets PIN/password.
- With lock screen: Read prompts for user auth.
- Grace period: Reads within validity window skip extra prompts.
-
Update/Remove: Verified with same
preQuery → auth → operation → postQuerypattern. - Data integrity: Returned secret matches last saved; remove clears it.
Related Documents or Links
- HarmonyOS Asset Store Service (Secure Kit) API reference
- HarmonyOS UserIAM (PIN/Face/Fingerprint) integration guide




Top comments (0)