前言
在iOS开发中Keychain
是一个非常安全的存储系统,用于保存敏感信息,如密码、证书、密钥等。与 NSUserDefaults
或文件系统不同,Keychain 提供了更高的安全性,因为它对数据进行了加密,并且只有经过授权的应用程序才能访问存储的数据。那么在鸿蒙里面对应的是什么呢?
1、关键资产(@ohos.security.asset)
在鸿蒙里面也有类似的东西,叫做关键资产(@ohos.security.asset
),关键资产存储服务提供了用户短敏感数据的安全存储及管理能力。其中,短敏感数据可以是密码类(账号/密码)、Token类(应用凭据)、其他关键明文(如银行卡号)等长度较短的用户敏感数据。
从API version 11 开始支持
使用关键资产需要导入模块AssetStoreKit
import { asset } from '@kit.AssetStoreKit';
2、asset常用操作
version 11 开始支持,异步方法,如下
-
asset.add:add(attributes: AssetMap): Promise,新增一条关键资产,使用Promise方式异步返回结果。
-
asset.remove:removeSync(query: AssetMap): void,删除符合条件的一条或多条关键资产,使用异步方式。
-
asset.update:update(query: AssetMap, attributesToUpdate: AssetMap): Promise,更新符合条件的一条关键资产,使用Promise方式异步返回结果。
-
asset.query:query(query: AssetMap): Promise<Array>,查询一条或多条符合条件的关键资产。若查询需要用户认证的关键资产,则需要在本函数前调用asset.preQuery,在本函数后调用asset.postQuery,使用Promise回调异步返回结果。
-
asset.preQuery:preQuery(query: AssetMap): Promise,查询的预处理,用于需要用户认证的关键资产。在用户认证成功后,应当随后调用asset.query、asset.postQuery。使用Promise方式异步返回结果。
-
asset.postQuery:postQuery(handle: AssetMap): Promise,查询的后置处理,用于需要用户认证的关键资产。需与asset.preQuery函数成对出现。使用Promise方式异步返回结果。
version 12 开始支持,同步方法,如下
-
asset.addSync:新增一条关键资产,使用Promise方式同步步返回结果。
-
asset.removeSync:removeSync(query: AssetMap): void,删除符合条件的一条或多条关键资产,使用同步方式。
-
asset.addSync:新增一条关键资产,使用Promise方式同步步返回结果。
-
asset.removeSync:removeSync(query: AssetMap): void,删除符合条件的一条或多条关键资产,使用同步方式。
-
asset.updateSync:updateSync(query: AssetMap, attributesToUpdate: AssetMap): void,更新符合条件的一条关键资产,使用同步方式返回结果。
-
asset.querySync:querySync(query: AssetMap): Array,查询一条或多条符合条件的关键资产。若查询需要用户认证的关键资产,则需要在本函数前调用asset.preQuerySync,在本函数后调用asset.postQuerySync,使用同步方式返回结果。
-
asset.preQuerySync:preQuerySync(query: AssetMap): Uint8Array,查询的预处理,用于需要用户认证的关键资产。在用户认证成功后,应当随后调用asset.querySync、asset.postQuerySync。使用同步方式返回结果。
-
asset.postQuerySync:postQuerySync(handle: AssetMap): void,查询的后置处理,用于需要用户认证的关键资产。需与asset.preQuerySync函数成对出现。使用同步方式返回结果。
关键资产需要使用到的系统能力: SystemCapability.Security.Asset
3、asset的封装使用
在iOS中使用Keychain
比较常见的功能是存储一个值作为设备唯一标识,那么asset
也以此作为示例封装一个,刚好前阵子项目里面也使用了。我也封装了一个工具类hmDeviceTools
。
3.1 导入需要的头文件
import { util } from '@kit.ArkTS'
import { asset } from '@kit.AssetStoreKit';
import { BusinessError } from '@kit.BasicServicesKit';
3.2 封装工具类
hmDeviceTools
类内容
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
同步来写了,后面的方法都是使用同步来实现。
/**
* 设置数据
* @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 获取数据
/**
* 获取数据
* @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
/**
* 判断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 查询数据
/**
* 查找数据
* @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 更新数据
/**
* 查找数据
* @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;
}
}
使用到的其他函数
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