2023/10/17
作者: 张 君子, 大池 陆斗, 小岩井 航介 产品开发1部
在不久前公开的Android14中,谷歌追加了可以用于管理通行密钥(Passkey)的Credential Provider API。 正好KDDI社内有一个通过实际开发app和产品,温习并活用技术知识的开发体验活动,我们便在这次活动中试作了一个通行密钥管理APP。 有关开发体验活动的详细将会在未来发布的博文中进行介绍,敬请期待!
注意:本篇博文记载的内容仅为作者们的个人意见,并非公司的观点。 在本篇博文中提及的源代码仅作调查、试作用。不保证实际应用时的正常运行。 另外,写作本篇博文时,Credential Provider API为beta02版。而这一API的用法可能会随后续的更新而有所改变。 若考虑实际应用本篇博文中的内容,请务必多加注意。
APP的源代码公开地址如下: https://github.com/ko-koiwai/MyCredentialManager
本篇博客的日语版,英语版地址如下。
简单来说,通行密钥是一种不需要将密码等秘密信息发送至外部的身份验证方式,它比密码更具安全性,如今越来越多的网页服务和软件加入了对通行密钥的支持。 更加详细的说明请参照其他的相关博文或百科,在这里就不加以赘述了。
Android在2022年12月开始支持通行密钥的使用,而使用的前提是每一个网页生成的通行密钥都与Google账户相关联,并由Google账户进行统一管理。 与通行密钥的严格管理相对的,至今为止的各种密码则是由用户本人自行管理。用户可以选择各种管理方式:例如把密码记在笔记本上,或者选择使用市场上通用的密码管理软件。au(我社)的数据云存(データお預かり)APP也提供了密码管理功能。(https://www.au.com/mobile/service/data-oazukari/passwords/)
为了实现更类似于密码的用户体验,从Android14开始,非OS的第三方密码管理软件也将可以对通行密钥进行管理,而实现了这一改变的便是Credential Provider API。
为了更深入理解通行密钥的功能和原理,我们利用上述API,实际制作了管理通行密钥的APP。
我们从2023年6月月末着手开发。当时的Credential Provider API是alpha版,相关说明文件也不甚齐全。 我们参考着API参考文件(https://developer.android.com/jetpack/androidx/releases/credentials) 和AOSP上的API源码 (https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/credentials/credentials/src/main/java/androidx/credentials) 边猜边试地进行了实装。 这对连PendingIntent都没理解我们来说难度相当高。实际操作中途还发生了实装完android.credentials之后却无法正确运行的问题。后来经过调查,我们才发觉应该实装的是androidx.credentials,因而不得不进行了大规模的重装。
7月27日,Google公司公开了Credential Manager 导入官方指南。(https://developer.android.com/training/sign-in/credential-provider) 我们参照着这一指南一点点地对APP进行了修正。 但由于日语版指南的信息更新慢,情报过时,在真正做到成功登陆通行密钥、并且进行认证这一结果还是花了我们很长的时间。
Credential Provider API在通行密钥的注册阶段和使用通行密钥进行验证阶段两个阶段中分别实行两次信息交换。 首先,来自浏览器的通行密钥的注册和验证请求由继承了CredentialProviderService()的服务取得。这些请求分别由重载的onBeginCreateCredentialRequest、onBeginGetCredentialRequest接收。收到的请求则通过PendingIntent转送至APP的Activity。 在这之后,被转送的请求分别被用来进行通行密钥的注册和验证,响应内容由PendingIntent返回至服务提供方。 最后,接收到了响应的onBeginCreateCredentialRequest、onBeginGetCredenttialRequest根据各自的结果调用callback函数,完成全部流程。
简单来说,我们在这里提到的通行密钥是每个网页生成的公钥密钥组合。 用户注册通行密钥时,通行密钥的公钥被送信至服务器端。而在用户实际使用通行密钥时,它将用密钥为网页返回的凭证署名,并将署名结果发送给服务器。 在保存密钥时,通行密钥管理APP需要将网页和用户划为一组进行保存,而具体的保存方法则由APP开发者自己决定。密钥是非常重要的身份情报,在保存时要注意绝不能简简单单就被他人获取到。 本次开发的APP主要着眼于理解通行密钥的原理和Credential Provider API的使用方法,因而我们在保存通行密钥这一环节上,选择了较为简单的将内容转换成json保存在SharedPreference的方式。
在经由Credential Provider API注册和使用通行密钥这一过程中,我们需要生成符合W3C WebAuthn的定义的标准规格的数据。这个数据规格也比较复杂,在进行实装时费了一番功夫。
虽然整体来说按照Google给出的说明文件(英文版)进行安装就没什么大问题,但是在途中还是有一些需要单独实装的部分。 其中主要为下述的3个部分:凭证的保存、RPID的验证、符合W3C WebAuthn定义的标准规格的数据的保存。
Google的实装说明书中,生成的密钥、RP和Credential ID的组合需要由APP单独进行保存。 本次开发中,安全保存凭证并非预定的实装内容,但是为了降低未来实装保存处理部分的难度,我们进行了数据模型(data class)定义和保存处理的抽象化。
通行密钥是与登陆网站的域名(也就是RP ID)绑定在一起进行管理的。在注册之后和再次登陆时也是以RP ID相同为前提,使用密钥进行署名。由于这是通行密钥应对钓鱼欺诈的大前提,我么需要防范有恶意的网站或APP伪装RP ID试图进行注册的情况。 与此同时,由于浏览器可以对任意的RP ID发送请求,通行密钥管理APP有必要决定应该信任哪些浏览器。 在有关这一点的验证上,我们使用了CallingAppInfo.getOrigin()。使用这一函数时需要json文件作为参数,而Google的密码管理软件使用的浏览器一览表公开在下面的地址,有需要的话可以直接使用。(https://gstatic.com/gpm-passkeys-privileged-apps/apps.json)
不过,要注意的是,这个表仅仅是Google公司出于好意公开的、Google Password Manager在利用的一览表,并非Google公司或Android官方所认可的可信任浏览器一览表。
我们可以通过结合androidx.credentials.webauthn.AuthenticatorAttestationResponse 、androidx.credentials.webauthn.FidoPublicKeyCredential、 androidx.credentials.CreatePublicKeyCredentialResponse生成返回给浏览器的json文件。但是目前光做到这一步还不够。
首先,我们需要对AuthenticatorAttestationResponse设定credentialPublicKey,而这需要以CBOR(RFC7049)形式表示的公钥——COSE(RFC9052)。我们使用了下述代码,把ES256形式的公钥转换成了COSE,然后根据验证器的后续动作模仿着进行了实装。
private fun getPublicKeyFromKeyPair(keyPair: KeyPair?): ByteArray { // credentialPublicKey CBOR if (keyPair==null) return ByteArray(0) if (keyPair.public !is ECPublicKey) return ByteArray(0) val ecPubKey = keyPair.public as ECPublicKey val ecPoint: ECPoint = ecPubKey.w // for now, only covers ES256 if (ecPoint.affineX.bitLength() > 256 || ecPoint.affineY.bitLength() > 256) return ByteArray(0) val byteX = bigIntToByteArray32(ecPoint.affineX) val byteY = bigIntToByteArray32(ecPoint.affineY) // refer to RFC9052 Section 7 for details return "A5010203262001215820".chunked(2).map { it.toInt(16).toByte() }.toByteArray() + byteX+ "225820".chunked(2).map { it.toInt(16).toByte() }.toByteArray() + byteY } private fun bigIntToByteArray32(bigInteger: BigInteger):ByteArray{ var ba = bigInteger.toByteArray() if(ba.size < 32) { // append zeros in front ba = ByteArray(32) + ba } // get the last 32 bytes as bigint conversion sometimes put extra zeros at front return ba.copyOfRange(ba.size - 32, ba.size) }
下面的网站可以更容易地对CBOR进行验证。 https://cbor.me
另外,在写这一篇博文的当下,如果我们想要让Chrome去识别由API生成的json文件,必须要采用目前尚且不支持W3C WebAuthn认证标准的Easy Accessor Field。因此,我们需要给API生成的json加入publicKeyAlgorithm、publicKey、authenticatorData的每个项目之后,再将其返回给浏览器。
这一次试做的通行密钥管理APP可以在webauthn.io, webauthn.me等测试网站上进行使用。要注意的是,webauthn.me默认不使用residentKey,这会导致测试时只显示Android标准FIDO2的窗口。为了回避这一状况,使用时需要在Debugger上进行authenticatorSelection.residentKey = preferred, 或者requireResidentKey的设定。
这一次试做的通行密钥管理APP可以在浏览器上使用,但尚且不能在其他APP上使用。 在Android的APP上使用通行密钥有两种情况:一种是以APP的署名为主值,另一种是以网页域名为主值。选择网页域名时,我们必须要验证眼前的APP是否是这个网页域名的持有者开发的APP。(否则这将成为钓鱼欺诈的温床) 而我们还可以通过使用DeepLink的Digiral Asser Linker机制验证APP的署名值,遗憾的是本次活动没那么多时间,因而最终没有进行实装。
那么,在Mac、iOS、Android、Windows均各自提供了OS标准通行密钥的现在,使用Credential Provider API的第三方APP进行通行密钥管理究竟有什么好处呢?
首先,第三方APP使得不同的平台之间的通行密钥可以互通了。不同OS的设备也好,Windows系统的电脑与移动端设备之间也好,都将可以通过第三方管理APP共同保存管理通行密钥。 另外,在必须要与他人共享密码时(例如家人之间或者公司内部等),共享通行密钥也将成为新的可能。
当然在上述优点之外,第三方通行密钥管理APP同样存在一些有待解决的问题。 当我们站在提供通行密钥注册和登陆服务的服务提供方角度上时,至今为止的通行密钥由OS进行统一管理,在一定程度上可以保证通行密钥的安全性。但是未来,服务提供方将无法得知通行密钥由谁进行管理。密码管理APP也偶有听说情报泄漏的情况,通行密钥的管理也是同样,无法保证绝对不会发生情报泄漏。 万一通行密钥发生情报泄漏时,如何保证使用服务的用户的个人信息和资产也是未来需要考虑的问题。 话说回来,至少在和密码进行比较时,通行密钥拥有不会被重复使用、不会被篡改的巨大优势。通过导入Credential Provider API,我们也可以期待通行密钥的进一步普及以及网络整体安全性的提高。
在Credential Manager API的实装中,我们得到了FIDO同盟的Google公司的Adam Langley、Jerry Shi;AgileBits公司(1Password)的Rene Leveille;Dashlane公司的Rew Islam等人的帮助,在此向他们致以感谢。