HarmonyOS API 26 UKey PIN 授权门禁:状态查询、失败归因与资源闭环
HarmonyOS API 26 UKey PIN 授权门禁状态查询、失败归因与资源闭环外接 UKey 的签名流程里最危险的不是“PIN 输入框没弹出来”而是页面把“用户刚刚验证过”当成一张长期通行证。资源被关闭、操作被取消、扩展返回错误甚至另一个页面切换了证书后如果应用仍拿旧状态继续签名就会把交互状态和真实密钥资源错配。本文围绕一条可审计的应用侧路径展开用系统证书选择流程取得资源标识打开资源在每次高权限操作前查询 PIN 状态只有明确处于认证成功态才放行操作结束无论成功失败都关闭资源异常发生时保留平台主错误码并在 API 26 可用的条件下立刻读取扩展错误信息。文中代码分为两类标明“API 示例”的部分对应官方接口形态标明“应用策略”的部分是可单测的业务边界不代表 HarmonyOS 编译或设备联调已完成。先把资源、认证和签名结果拆开实际排查时三种状态经常被揉成一个isAuthorized布尔值后来很难知道失败在资源打开、PIN 认证还是签名操作本身。至少要保留三份互不替代的证据resourceState表示当前资源是否由本轮流程打开pinState是系统/扩展针对该资源返回的认证状态operationResult才表示签名动作的业务结果。观察到的状态可以做什么不能据此推断什么资源未打开展示重试或重新选择入口PIN 已认证资源已打开PIN 未认证引导进入受控认证流程私钥可以签名PIN 已认证放行当前资源上的授权动作其他资源或未来会话也已授权签名返回成功保存本次操作结果用户身份或业务载荷无需再核验关键是把授权结论限定在“当前 resourceId、当前资源会话、当前待执行动作”上。不要将上一次签名成功写成全局状态也不要把客户端页面变量当成密钥管理服务的权威认证状态。资源标识必须来自受支持的获取路径官方说明将resourceId用作外部密钥资源标识证书选择场景可由证书管理系统的授权对话框返回API 26 起密钥生成或导入等不需要证书选择的场景可使用getResourceId。打开资源后再执行密钥操作结束后关闭资源。不要在业务代码里自行拼一个“看起来像 JSON”的标识去替代系统返回值。推荐把系统返回的标识包成短生命周期句柄界面层只保存引用不把完整资源标识写进埋点或普通日志。页面返回、用户切换证书、窗口关闭、应用流程取消都应让当前句柄失效。interfaceSelectedKeyResource{resourceId:string;selectionId:string;selectedAt:number;}functionisCurrentSelection(selected:SelectedKeyResource,activeSelectionId:string,now:number):boolean{returnselected.resourceId.length0selected.selectionIdactiveSelectionIdnowselected.selectedAt;}这里的selectionId和时间字段是应用自己的防串用上下文并非 HUKS 参数。它们能阻止 UI 延迟回调把旧证书的状态误用于新选择但不能代替平台对资源与 PIN 状态的校验。每次高权限动作前查询 PIN 状态官方 ArkTS 指南说明从 API 22 开始huksExternalCrypto.getUkeyPinAuthState可查询 PIN 认证状态查询所需resourceId应来自证书管理系统的授权流程。API 返回枚举值而非应用自定义缓存。下面保留官方接口的关键调用形式外围错误类型映射属于应用层包装需按目标 SDK 声明补齐。import{huksExternalCrypto}fromkit.UniversalKeystoreKit;asyncfunctionqueryUkeyPinState(resourceId:string):PromisehuksExternalCrypto.HuksExternalPinAuthState{constextProperties:ArrayhuksExternalCrypto.HuksExternalCryptoParam[];returnawaithuksExternalCrypto.getUkeyPinAuthState(resourceId,extProperties);}不要只在页面加载时查询一次然后在页面存活期间无限复用。资源关闭会清理关联 PIN 认证状态与会话因此每次签名动作应在同一资源上下文内重新查询认证结果不等于跨页面、跨资源的授权缓存。让“未知”与“未认证”都走拒绝路径门禁逻辑要遵循 fail closed明确成功才能继续未认证则进入认证步骤查询异常、枚举值未知、资源不匹配、选择上下文过期都不能转成成功。下面的策略函数只处理纯数据适合脱离设备环境做单元测试。typePinGateStateauthenticated|not-authenticated|unknown;typeGateDecisionallow-current-action|request-auth|deny-and-reopen;functiondecidePinGate(resourceMatches:boolean,state:PinGateState):GateDecision{if(!resourceMatches||stateunknown)returndeny-and-reopen;if(stateauthenticated)returnallow-current-action;returnrequest-auth;}真实业务还要把返回值映射到明确的 UI 状态request-auth不能自动重放 PIN也不能绕过用户确认deny-and-reopen应提示重新选择或打开资源并清除页面中的旧授权提示。对付款、签署协议等业务操作授权成功只是技术门槛业务还需再次确认待签数据的摘要和接收方。资源关闭是认证状态边界不是可选清理官方资源管理说明要求先打开资源再执行后续密钥操作完成后关闭资源关闭时扩展会清理该资源关联的 PIN 认证状态和会话句柄。因而应把资源生命周期收进单一 service而不是由多个页面分别负责“记得关闭”。interfaceResourceLifecyclePortT{open(resourceId:string):PromiseT;close(resourceId:string):Promisevoid;}asyncfunctionwithUkeyResourceT,R(port:ResourceLifecyclePortT,resourceId:string,action:(handle:T)PromiseR,reportCleanupFailure:(error:unknown)void):PromiseR{consthandleawaitport.open(resourceId);letactionFailedfalse;try{returnawaitaction(handle);}catch(error){actionFailedtrue;throwerror;}finally{try{awaitport.close(resourceId);}catch(closeError){reportCleanupFailure(closeError);if(!actionFailed)throwcloseError;}}}这是生命周期包装的应用侧示意open/close的实际适配应调用目标 API 对应的 HUKS 接口并转换平台错误。回调只接收脱敏后的清理故障分类有主操作异常时保留主错误单独记录关闭失败主操作成功但关闭失败时则将清理故障返回给调用者。若产品要重试先重新打开资源并重新查询 PIN 状态不复用已关闭资源的认证结论。API 26 错误归因要紧跟失败点从 API 26 开始huksExternalCrypto.getErrorInfo()可读取最近一次外部密钥管理扩展操作返回的详细信息。它只补充扩展侧错误HUKS 自身错误仍通过接口异常抛出。官方还特别说明扩展未提供详细错误时errno可能为 0 或保留上次值、描述可能为空因此调用方必须保留当前接口抛出的主错误码不能把旧的扩展错误误贴到新故障上。interfaceFailureEvidence{operation:open|pin-state|authenticate|sign|close;platformCode:number|null;extensionCode:number|null;extensionDescriptionPresent:boolean;}functionbuildFailureEvidence(operation:FailureEvidence[operation],platformCode:number|null,extensionCode:number,extensionDescription:string):FailureEvidence{return{operation,platformCode,extensionCode:extensionCode0?null:extensionCode,extensionDescriptionPresent:extensionDescription.length0};}在catch分支中应立即读取扩展信息并复制必要字段再异步写审计队列。只记录错误阶段、稳定的业务 requestId、平台错误码、非零扩展码和描述是否存在不要记录 PIN 原文、密钥材料、完整证书内容或未经清洗的扩展文本。扩展描述可以含供应商或设备细节应受控保存并限制访问。串行化同一资源的认证与签名操作双击“签名”、页面自动重试和后台恢复可能同时触发同一资源的open → PIN状态查询 → sign → close。如果两个任务交错A 任务关闭资源后B 任务仍以旧状态继续UI 却可能展示“验证成功”。在应用层为同一resourceId建立互斥队列或操作租约并且每个动作绑定唯一 requestId。interfaceSignIntent{requestId:string;selectionId:string;payloadDigest:string;}functioncanContinueIntent(intent:SignIntent,activeSelectionId:string,finishedRequestIds:Setstring):boolean{returnintent.requestId.length0intent.selectionIdactiveSelectionId!finishedRequestIds.has(intent.requestId);}这个纯逻辑检查只防止本地重复触发或旧页面回调真正的签名仍必须由资源 service 在打开资源后重新查 PIN 状态再调用对应平台操作。对于用户取消的动作requestId 状态应进入cancelled而不是failed恢复时不能静默重播新一次用户确认应产生新 requestId。PIN 与错误文本按敏感数据处理PIN 是用户的认证秘密不应写入偏好设置、数据库、崩溃日志、埋点参数或本地明文调试文件。输入组件提交后尽可能缩短字符串持有时间取消或页面退出时丢弃草稿。不要在应用中自行实现连续错误次数、解锁策略或硬件资源访问控制应遵守 UKey/扩展提供的认证行为不绕过认证界面。官方 UKey PIN 说明将“硬件设备 PIN”作为双因素访问保护并指出 PIN 校验在 UKey 设备内完成。应用负责的是最小化留存、透明告知和安全调用不应承诺自己可以读取私钥也不应把“查询到已认证”包装成对用户身份的普遍证明。日志字段用分类值和不可逆业务关联标识避免原始凭证及完整 resourceId 暴露。用状态矩阵验证门禁与清理顺序可先对纯策略模块做单测再在目标 API 和实际扩展环境验证平台行为。以下矩阵重点验证边界条件而不是只跑一次成功路径。场景期望 UI / service 行为通过证据资源打开失败不查询 PIN、不签名返回可重试错误open失败事件与 requestIdPIN 未认证保持操作待确认进入受支持的认证流程查询结果为未认证不产生签名调用PIN 查询异常fail closed提示重开资源当前平台码被保留未调用签名资源选择被替换旧 selectionId 的结果丢弃旧回调不更新新页面状态签名成功或失败均走资源关闭关闭错误单独记录生命周期顺序与两个错误槽位同资源快速双击一个操作执行重复请求被拒绝或合并requestId 去重及调用次数API 26 扩展错误为空主错误仍可定位不沿用旧扩展描述platformCode 有值extensionCode 为空用户主动取消不自动重试、不继续签名状态为 cancelled未发生副作用本地纯逻辑测试边界下面的测试只验证门禁决策、资源上下文隔离、重复请求和错误证据组装不调用 HUKS、不替代 HarmonyOS 编译也不代表真机 UKey 验收。常见故障先看哪一层现象优先检查建议动作状态查询成功但签名被拒是否为同一 resourceId、资源是否被关闭重开重新打开资源并在动作前重新查询PIN 失败但扩展码看起来是旧值是否在当前失败后立即获取getErrorInfo以当前异常主码为主扩展码只作补充用户取消后又触发签名Promise 拒绝是否被当作普通错误自动重试将取消建模为终态要求新的显式确认页面显示授权通过但资源已切换旧请求是否携带 selectionId丢弃旧结果清除旧授权态连续签名时偶发资源不可用是否有并发 open/close 交错按 resourceId 串行执行生命周期日志泄露认证信息是否序列化了 PIN、完整资源标识或错误原文采用白名单字段和敏感值脱敏官方指南可用于核对具体接口和边界PIN 认证状态查询ArkTS、UKey PIN 认证介绍与规格、API 26 获取外部密钥扩展错误信息、外部密钥资源管理说明。涉及 API 接入时以项目使用的 SDK 声明和设备/扩展支持范围为准。收口把认证判断压在动作边界可靠的 UKey 流程不是“登录时验一次 PIN”而是把资源打开、认证状态查询、敏感动作、错误归因和资源关闭串成一个短事务。交付前可按下面五项逐条复核PIN 状态只对当前资源、当前会话和当前动作有效状态查询失败一律不签名。用户取消、换证书或页面退出会让旧操作失效不自动提交旧 intent。API 26 扩展错误在失败点立即读取接口主错误码始终单独保留。openResource成功后的每条路径都能走到资源关闭清理错误不会吞掉主操作错误。PIN、私钥材料、完整证书和原始资源标识不进入普通日志、埋点或持久化草稿。这样做的收益不是让失败消失而是让每次拒绝都可解释、每份认证状态都有明确边界、每次资源使用都能收尾。若目标设备、驱动扩展或 SDK 版本不满足对应能力应在能力探测和产品提示层显式降级不用“本地状态看起来已认证”绕过服务端门禁。