DEV Community

HarmonyOS
HarmonyOS

Posted on

Implementing User-Authenticated Access to Secrets with Asset Store Service

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.

kbs--109e753b2e034847a38d6a56818d9ead-1257d1.png

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:

kbs--f8543a7f6d324fcd875b132eca5e5c7a-17fb86.png

Implementation Steps

  1. Choose an alias for the secret (e.g., secure_api_token_v1).
  2. Save the secret with authentication requirements:
    • ACCESSIBILITY=DEVICE_UNLOCKED
    • REQUIRE_PASSWORD_SET=true
    • AUTH_TYPE=ANY
    • Optional: AUTH_VALIDITY_PERIOD=30–120s
  3. 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.
  4. Update and Remove follow the same pattern if user auth is required.
  5. 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);
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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')
  }
}
Enter fullscreen mode Exit fullscreen mode

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 → postQuery pattern.
  • Data integrity: Returned secret matches last saved; remove clears it.

cke_18644.png cke_20877.png

Related Documents or Links

Written by Omer Basri Okcu

Top comments (0)