在 iOS 开发中 Keychain 是一个非常安全的存储系统,用于保存敏感信息,如密码、证书、密钥等。与文件系统不同,Keychain 提供了更高的安全性,因为它对数据进行了加密,并且只有经过授权的应用程序才能访问存储的数据。那么在鸿蒙里面对应的是什么呢?
在鸿蒙里面也有类似的东西,叫做关键资产(@ohos.security.asset),关键资产存储服务提供了用户短敏感数据的安全存储及管理能力。其中,短敏感数据可以是密码类(账号/密码)、Token类(应用凭据)、其他关键明文(如银行卡号)等长度较短的用户敏感数据。
关键资产的安全存储,依赖底层的通用密钥库系统。具体来说,关键资产的加/解密操作以及访问控制校验,都由通用密钥库系统在安全环境(如可信执行环境)中完成,即使系统被攻破,也能保证用户敏感数据不发生泄露。其中,关键资产的加/解密使用AES256-GCM算法。
关键资产的访问可分为 4 类(可查看本文第4章节),基于属主的访问控制、基于锁屏状态的访问控制、基于锁屏密码设置状态的访问控制、基于用户认证的访问控制,业务可根据实际情况决定是否开启(如扫脸验证、解锁验证、密码验证等),本次文章举例的开发案例均采用默认保护等级。
AssetStoreKit:import { asset } from '@kit.AssetStoreKit';

| 方法 | 描述 |
|---|---|
| asset.add | 新增一条关键资产。 |
| asset.remove | 删除符合条件的一条或多条关键资产。 |
| asset.update | 更新符合条件的一条关键资产。 |
| asset.query | 查询一条或多条符合条件的关键资产。若查询需要用户认证的关键资产,则需要在本函数前调用asset.preQuery,在本函数后调用asset.postQuery。 |
| asset.preQuery | 查询的预处理,用于需要用户认证的关键资产。在用户认证成功后,应当随后调用asset.query、asset.postQuery。 |
| asset.postQuery | 查询的后置处理,用于需要用户认证的关键资产。需与asset.preQuery函数成对出现。 |
/**
* 新增一条关键资产
* [url=home.php?mod=space&uid=3142012]@param[/url] key
* @param value
* @returns
*/
add(key: string, value: string) {
let result: Boolean
let attr: asset.AssetMap = new Map();
// 关键资产别名,每条关键资产的唯一索引。
// 类型为Uint8Array,长度为1-256字节。
attr.set(asset.Tag.SECRET, this.string2Array(value));
// 关键资产明文。
// 类型为Uint8Array,长度为1-1024字节
attr.set(asset.Tag.ALIAS, this.string2Array(key))
// 关键资产同步类型>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 (this.isHasKey(key)) {
result = this.updateAssetMap(attr);
} else {
try {
if (canIUse("SystemCapability.Security.Asset")) {
asset.addSync(attr);
result = true
}
result = false
} catch (error) {
let err = error as BusinessError;
LogUtil.e(`Failed to add Asset. Code is ${err.code}, message is ${err.message}`);
result = false
}
}
return result
}

/**
* 查询
* @param key
* @returns
*/
query(key: string) {
let query: asset.AssetMap = new Map();
// 关键资产别名,每条关键资产的唯一索引。
// 类型为Uint8Array,长度为1-256字节。
query.set(asset.Tag.ALIAS, this.string2Array(key));
// 关键资产查询返回的结果类型。
query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);
// query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ATTRIBUTES); // 此处表示仅返回关键资产属性,不包含关键资产明文
try {
if (canIUse("SystemCapability.Security.Asset")) {
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 = this.array2String(secret);
return secretStr;
}
}
}
} catch (error) {
let err = error as BusinessError;
LogUtil.e(TAG, `Failed to query Asset. Code is ${err.code}, message is ${err.message}`);
return "";
}
return ""
}

/**
* 查询资产key是否存在
* @param key
* @returns
*/
isHasKey(key: string): Boolean {
if (canIUse("SystemCapability.Security.Asset")) {
let query: asset.AssetMap = new Map();
// 关键资产别名,每条关键资产的唯一索引。
// 类型为Uint8Array,长度为1-256字节。
query.set(asset.Tag.ALIAS, this.string2Array(key));
// 关键资产查询返回的结果类型。
query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);
const res = this.queryAssetMap(query);
if (!res || res.length < 1) {
return false;
}
return true;
}
return false;
}

/**
* 查询资产 key 的 assetMaps 数据
* @param query
* @returns
*/
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;
LogUtil.e(TAG, `Failed to query Asset. Code is ${err.code}, message is ${err.message}`);
return assetMaps;
}
}

/**
* 修改资产数据
* @param q
* @returns
*/
updateAssetMap(q: asset.AssetMap): Boolean {
try {
if (canIUse("SystemCapability.Security.Asset")) {
let query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, q.get(asset.Tag.ALIAS)!);
let attrsToUpdate: asset.AssetMap = new Map();
attrsToUpdate.set(asset.Tag.SECRET, q.get(asset.Tag.SECRET)!);
asset.updateSync(query, attrsToUpdate);
return true
}
return false
} catch (error) {
const err = error as BusinessError;
LogUtil.e(TAG, `Failed to update Asset. Code is ${err.code}, message is ${err.message}`);
return false;
}
}

/**
* 删除一条关键资产
* @param key
*/
remove(key: string) {
let query: asset.AssetMap = new Map();
// 关键资产别名,每条关键资产的唯一索引。
query.set(asset.Tag.ALIAS, this.string2Array(key));
try {
if (canIUse("SystemCapability.Security.Asset")) {
asset.removeSync(query);
return true;
}
return false;
} catch (error) {
let err = error as BusinessError;
LogUtil.e(TAG, `Failed to remove Asset. Code is ${err.code}, message is ${err.message}`);
return false;
}
}

string2Array(str: string): Uint8Array {
let textEncoder = new util.TextEncoder();
return textEncoder.encodeInto(str);
}
array2String(str: Uint8Array): string {
let textDecoder = new util.TextDecoder();
return textDecoder.decodeToString(str);
}

基于属主的访问控制: 所有的关键资产都受属主访问控制保护,业务无需设置。
基于锁屏状态的访问控制: 分为以下三种保护等级(安全性依次递增),业务可根据实际情况设置任意一种,若不设置,则默认保护等级为“首次解锁后可访问”。
基于锁屏密码设置状态的访问控制: 该访问控制默认不开启,业务可根据实际情况决定是否开启。
基于用户认证的访问控制: 该访问控制默认不开启,业务可根据实际情况决定是否开启。
华为官网:@ohos.security.asset (关键资产存储服务)