【鸿蒙NEXT】鸿蒙里面类似iOS的Keychain——关键资产(@ohos.security.asset)实现设备唯一标识

前言

在iOS开发中Keychain 是一个非常安全的存储系统,用于保存敏感信息,如密码、证书、密钥等。与 NSUserDefaults 或文件系统不同,Keychain 提供了更高的安全性,因为它对数据进行了加密,并且只有经过授权的应用程序才能访问存储的数据。那么在鸿蒙里面对应的是什么呢?

1、关键资产(@ohos.security.asset)

在鸿蒙里面也有类似的东西,叫做关键资产(@ohos.security.asset),关键资产存储服务提供了用户短敏感数据的安全存储及管理能力。其中,短敏感数据可以是密码类(账号/密码)、Token类(应用凭据)、其他关键明文(如银行卡号)等长度较短的用户敏感数据。

从API version 11 开始支持

使用关键资产需要导入模块AssetStoreKit

js 复制代码
import { asset } from '@kit.AssetStoreKit';

2、asset常用操作

version 11 开始支持,异步方法,如下

  1. asset.add:add(attributes: AssetMap): Promise,新增一条关键资产,使用Promise方式异步返回结果。

  2. asset.remove:removeSync(query: AssetMap): void,删除符合条件的一条或多条关键资产,使用异步方式。

  3. asset.update:update(query: AssetMap, attributesToUpdate: AssetMap): Promise,更新符合条件的一条关键资产,使用Promise方式异步返回结果。

  4. asset.query:query(query: AssetMap): Promise<Array>,查询一条或多条符合条件的关键资产。若查询需要用户认证的关键资产,则需要在本函数前调用asset.preQuery,在本函数后调用asset.postQuery,使用Promise回调异步返回结果。

  5. asset.preQuery:preQuery(query: AssetMap): Promise,查询的预处理,用于需要用户认证的关键资产。在用户认证成功后,应当随后调用asset.query、asset.postQuery。使用Promise方式异步返回结果。

  6. asset.postQuery:postQuery(handle: AssetMap): Promise,查询的后置处理,用于需要用户认证的关键资产。需与asset.preQuery函数成对出现。使用Promise方式异步返回结果。

    version 12 开始支持,同步方法,如下

  7. asset.addSync:新增一条关键资产,使用Promise方式同步步返回结果。

  8. asset.removeSync:removeSync(query: AssetMap): void,删除符合条件的一条或多条关键资产,使用同步方式。

  9. asset.addSync:新增一条关键资产,使用Promise方式同步步返回结果。

  10. asset.removeSync:removeSync(query: AssetMap): void,删除符合条件的一条或多条关键资产,使用同步方式。

  11. asset.updateSync:updateSync(query: AssetMap, attributesToUpdate: AssetMap): void,更新符合条件的一条关键资产,使用同步方式返回结果。

  12. asset.querySync:querySync(query: AssetMap): Array,查询一条或多条符合条件的关键资产。若查询需要用户认证的关键资产,则需要在本函数前调用asset.preQuerySync,在本函数后调用asset.postQuerySync,使用同步方式返回结果。

  13. asset.preQuerySync:preQuerySync(query: AssetMap): Uint8Array,查询的预处理,用于需要用户认证的关键资产。在用户认证成功后,应当随后调用asset.querySync、asset.postQuerySync。使用同步方式返回结果。

  14. asset.postQuerySync:postQuerySync(handle: AssetMap): void,查询的后置处理,用于需要用户认证的关键资产。需与asset.preQuerySync函数成对出现。使用同步方式返回结果。

关键资产需要使用到的系统能力: SystemCapability.Security.Asset

3、asset的封装使用

在iOS中使用Keychain 比较常见的功能是存储一个值作为设备唯一标识,那么asset也以此作为示例封装一个,刚好前阵子项目里面也使用了。我也封装了一个工具类hmDeviceTools

3.1 导入需要的头文件

js 复制代码
import { util } from '@kit.ArkTS'
import { asset } from '@kit.AssetStoreKit';
import { BusinessError } from '@kit.BasicServicesKit';

3.2 封装工具类

hmDeviceTools类内容

js 复制代码
export class hmDeviceTools {

  private static deviceIdCacheKey = "testdevice_id_cache_key" //testkey
  private static deviceId = ""

  /**
   * * 判断字符串是否为空
   * @param property 被检测的字符串
   * @return Boolean
   */
  static isEmpty(property?: string | null): Boolean {
    if (property == '' || property == null || property == undefined || property == 'undefined' ||
      property.length == 0) {
      return true
    }
    return false
  }

  /**
   * 获取设备id
   */
  static getDeviceId() {
    let deviceId = hmDeviceTools.deviceId
    //如果内存缓存为空,则从AssetStore中读取
    if (hmDeviceTools.isEmpty(deviceId)) {
      deviceId = getAssetMap(hmDeviceTools.deviceIdCacheKey)
    }
    //如果AssetStore中未读取到,则随机生成32位随机码,然后缓存到AssetStore中
    if (hmDeviceTools.isEmpty(deviceId)) {
      deviceId = util.generateRandomUUID(true).replace(new RegExp('-', "gm"), '')
      deviceId = deviceId.slice(0,Math.min(10,deviceId.length))//可以确保不会超出字符串的长度。
      setAssetMap(hmDeviceTools.deviceIdCacheKey, deviceId)
    }
    hmDeviceTools.deviceId = deviceId
    return deviceId
  }
}

getDeviceId函数里面,我是截取的10位,大家可以工具自己的具体业务来自行截取,或者使用使用generateRandomUUID返回的32位。

3.3 addSync 设置数据

既然有异步和同步可选,我当然是使用addSync同步来写了,后面的方法都是使用同步来实现。

js 复制代码
    
/**
 * 设置数据
 * @param key  要查找的索引
 * @param value 需要存的值
 */
function setAssetMap(key: string, value: string) {

  let attr: asset.AssetMap = new Map();
  let result: Boolean
  if (canIUse("SystemCapability.Security.Asset")) {
    // 关键资产别名,每条关键资产的唯一索引。
    // 类型为Uint8Array,长度为1-256字节。
    attr.set(asset.Tag.ALIAS, stringToArray(key));
    // 关键资产明文。
    // 类型为Uint8Array,长度为1-1024字节
    attr.set(asset.Tag.SECRET, stringToArray(value));

    // 关键资产同步类型>THIS_DEVICE只在本设备进行同步,如仅在本设备还原的备份场景。
    attr.set(asset.Tag.SYNC_TYPE, asset.SyncType.THIS_DEVICE);

    //枚举,新增关键资产时的冲突(如:别名相同)处理策略。OVERWRITE》抛出异常,由业务进行后续处理。
    // attr.set(asset.Tag.CONFLICT_RESOLUTION,asset.ConflictResolution.THROW_ERROR)
    // 在应用卸载时是否需要保留关键资产。
    // 需要权限: ohos.permission.STORE_PERSISTENT_DATA。
    // 类型为bool。
    // attr.set(asset.Tag.IS_PERSISTENT, true);//我项目里面没有使用就先注释了,后续有需要这个再打开,并且要设置对应权限
  }

  if (isHasKey(key)) {
    result = updateAssetMap(attr, attr);
  } else {
    try {
      asset.addSync(attr);
      result = true
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to add Asset. Code is ${err.code}, message is ${err.message}`);
      result = false
    }
  }

}

3.4 querySync 获取数据

js 复制代码
/**
 * 获取数据
 * @param key  要查找的索引
 * @returns string 表示操作的结果
 */
function getAssetMap(key: string): string {

  if (canIUse("SystemCapability.Security.Asset")) {

    let query: asset.AssetMap = new Map();
    // 关键资产别名,每条关键资产的唯一索引。
    // 类型为Uint8Array,长度为1-256字节。
    query.set(asset.Tag.ALIAS, stringToArray(key));
    //  关键资产查询返回的结果类型。
    query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);
    // query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ATTRIBUTES); // 此处表示仅返回关键资产属性,不包含关键资产明文

    try {

      let res: Array<asset.AssetMap> = asset.querySync(query);

      for (let i = 0; i < res.length; i++) {
        // parse the attribute.
        if (res[i] != null) {
          // parse the secret.
          let secret: Uint8Array = res[0].get(asset.Tag.SECRET) as Uint8Array;
          // parse uint8array to string
          let secretStr: string = arrayToString(secret);

          return secretStr;
        }

      }

    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to query Asset. Code is ${err.code}, message is ${err.message}`);
      return "";
    }
  }

  return "";
}

3.4 querySync 查询key

js 复制代码
/**
 * 判断key是否存在
 * @param key 要查找的索引
 * @returns Boolean 表示添加操作的结果
 */
function isHasKey(key: string): Boolean {

  if (canIUse("SystemCapability.Security.Asset")) {

    let query: asset.AssetMap = new Map();

    // 关键资产别名,每条关键资产的唯一索引。
    // 类型为Uint8Array,长度为1-256字节。
    query.set(asset.Tag.ALIAS, stringToArray(key));

    //  关键资产查询返回的结果类型。
    query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);

    const res = queryAssetMap(query);

    if (!res || res.length < 1) {
      return false;
    }

    return true;
  }

  return false;
}

3.5 querySync 查询数据

js 复制代码
/**
* 查找数据
* @param key  要查找的索引
* @returns Array<asset.AssetMap> 表示添加操作的结果
*/
function queryAssetMap(query: asset.AssetMap): Array<asset.AssetMap> {

 const assetMaps: asset.AssetMap[] = [];

 try {

   if (canIUse("SystemCapability.Security.Asset")) {
     const res: asset.AssetMap[] = asset.querySync(query);
     return res;
   }

   return assetMaps;

 } catch (error) {

   const err = error as BusinessError;
   console.error(`Failed to query Asset. Code is ${err.code}, message is ${err.message}`);
   return assetMaps;

 }
}

3.6 updateSync 更新数据

js 复制代码
/**
 * 查找数据
 * @param key  要查找的索引
 * @returns Array<asset.AssetMap> 表示添加操作的结果
 */
function queryAssetMap(query: asset.AssetMap): Array<asset.AssetMap> {

  const assetMaps: asset.AssetMap[] = [];

  try {

    if (canIUse("SystemCapability.Security.Asset")) {
      const res: asset.AssetMap[] = asset.querySync(query);
      return res;
    }

    return assetMaps;

  } catch (error) {

    const err = error as BusinessError;
    console.error(`Failed to query Asset. Code is ${err.code}, message is ${err.message}`);
    return assetMaps;

  }
}

使用到的其他函数

js 复制代码
function stringToArray(str: string): Uint8Array {
  let textEncoder = new util.TextEncoder();
  return textEncoder.encodeInto(str);
}

function arrayToString(arr: Uint8Array): string {

  let textDecoder = util.TextDecoder.create('utf-8', { fatal: false, ignoreBOM: true });
  let decodeToStringOptions: util.DecodeToStringOptions = {
    stream: false
  }

  let str = textDecoder.decodeToString(arr, decodeToStringOptions);

  return str;
}

4、特别说明

如果需要卸载之后获取的值不变,需要设置IS_PERSISTENT属性,需要申请ohos.permission.STORE_PERSISTENT_DATA权限。

完整项目的结构如下:

5、参考

1、华为官网:@ohos.security.asset (关键资产存储服务)

2、冉冉同学:【HarmonyOS NEXT】获取卸载APP后不变的设备ID

相关推荐
轻口味8 小时前
【每日学点鸿蒙知识】RelativeContainer组件、List回弹、Flutter方法调用、Profiler工具等
flutter·list·harmonyos
HarmonyOS开发者10 小时前
《HarmonyOS第一课》焕新升级,赋能开发者快速掌握鸿蒙应用开发
华为·harmonyos
塞尔维亚大汉11 小时前
【OpenHarmony】 鸿蒙网络请求库之httpclient
网络协议·harmonyos
NanoNerd11 小时前
使用 4 种主要方法将数据从 HTC 传输到华为
华为·传输·步骤
我是全栈架构师13 小时前
HarmonyOS Next ArkUI @State @Prop @Link @Provide @Consume笔记
harmonyos·arkts
guo_zhen_qian14 小时前
鸿蒙工程签名编译和上架
华为·harmonyos
执着的小火车15 小时前
【2024华为OD-E卷-100分-火星文计算】(题目+思路+Java&C++&Python解析)
java·数据结构·c++·算法·华为od·华为
IT考试认证16 小时前
2024年变题后华为数通H12-821更新题库
网络·华为·智能路由器·h12-821
个案命题16 小时前
纯血鸿蒙ArkUI选项卡布局详解
华为·harmonyos
HarderCoder16 小时前
仓颉中的类型型变
harmonyos