2023/10/17
Authors: Junzi Zhang, Rikuto Ohike, Kosuke Koiwai, Service Development Department 1.
当記事は、日本語版記事の翻訳版です。日本語版オリジナル記事は下記をご覧ください。
パスキー管理アプリ作ってみた : https://developers.kddi.com/blog/2hb9zXuXGEALOsLqCxXEHS
中国語版も公開しています。
用Credential Provider API试做了通行密钥管理APP : https://developers.kddi.com/blog/1RLrbZlhxjGzZ4fE5bzJmy
Now that Android 14 has been released, it's possible to manage passkeys using the Credential Provider API. So, we created a prototype app.
Note: The information provided here is the personal opinion of the authors and does not represent the company's position. Additionally, the source code presented here is for research and prototyping purposes; please be careful if you use it for production purposes.
Furthermore, please note that the version of the Credential Provider API at the time of writing this article was beta02, and there is a possibility that the API usage may have changed in subsequent updates.
The complete source code is available on GitHub at: https://github.com/ko-koiwai/MyCredentialManager
Simply put, a passkey is a public-private key pair created for each website. When registering a passkey on a website, the browser sends the public key, and when using the passkey, the browser sends the result signed with the private key in response to the challenge received from the website.
The passkey managing app must store the private key, which is linked to the website and the user; however, the method of storing the private key is up to the app. Since the private key is an important piece of information, the app must store it in a way that is not easily accessible. In this app, the focus is on understanding passkeys and how to use the Credential Provider API, so storage of the passkey is only temporarily kept by serializing it to JSON and storing it in SharedPreferences.
To register and use a passkey through the Credential Provider API, data must be generated according to the format specified in the W3C WebAuthn specification. This data format is a bit complicated, and implementation was difficult.
We began working on the implementation of the API at the end of June 2023, when it was still in alpha version. Adding to the difficulties, the implementation guide was not yet in place, so we had to imagine how to use the API from the API reference (https://developer.android.com/jetpack/androidx/releases/credentials) and actual API source code on AOSP (https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/credentials/credentials/src/main/java/androidx/credentials). We even struggled trying to implement with android.credentials. Eventually, we realized that we should have used androidx.credentials instead.
On July 27th, Google thankfully released a guide for implementing the Credential Provider API (https://developer.android.com/training/sign-in/credential-provider), which greatly helped us in working towards a functional implementation. However, since the Japanese language documentation was out-of-date, it still took us some time to be able to successfully register and authenticate passkeys.
In general, implementation of the Credential Management API can be done just by following the explanations provided by Google's implementation guide. However, there are parts that require custom implementation in the process. These include mainly the storage of the credentials, validation of the RP ID, and the formatting data conforming to the WebAuthn specification.
Google's implementation guide suggests that the app should store the created key pair, RP, and Credential ID on its own. Though we chose not to focus on safe storage of the credential as it falls beyond the scope of our project, we still have implemented data models (data class) and abstracted the storage process for easier implementation of the storage process in the future.
Passkeys are managed by the domain of the website (called RP ID). After registration, the signature is verified by using the private key, assuming the RP ID is matched at the login. This is a crucial measure against phishing, hence the need to prevent rogue web sites or apps from falsifying RP ID.
On the other hand, browsers can make requests to any RP ID, so as a passkey manager app, we need to decide which browsers to trust. Verification can be done using CallingAppInfo.getOrigin() with a JSON as an argument. A browser list used by Google Password Manager is made public and can be used: https://gstatic.com/gpm-passkeys-privileged-apps/apps.json, but please note that you should cache the list and this list should not be taken as an official or reliable list of trusted browser apps by Google or Android.
To create the JSON to be returned to the browser, we can use the androidx.credentials.webauthn.AuthenticatorAttestationResponse, androidx.credentials.webauthn.FidoPublicKeyCredential, and androidx.credentials.CreatePublicKeyCredentialResponse. However, this alone does not complete the process.
Firstly, for the AuthenticatorAttestationResponse, we need to set the credentialPublicKey. This requires a public key expressed in CBOR (RFC7049) format, named COSE (RFC9052). Though it can be tough to understand all the specifications, for converting an ES256 public key to COSE, we were able to convert by the following code by observing the actual behavior of other authenticators and implementing based on trial and error.
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) }
Furthermore, at the time of writing this article, Chrome did not recognize the JSON created by the API unless the Easy Accessor Fields, which is not yet incorporated into the W3C WebAuthn specification, were added. Therefore, we added the following items - publicKeyAlgorithm, publicKey, and authenticatorData - to the JSON created by the API before returning it to the browser.
The passkey manager app we prototyped was usable on test sites such as webauthn.io and webauthn.me. One issue is that webauthn.me does not set the residentKey as default, and as a result, the Android standard FIDO2 dialog is displayed. To fix this, you need to set the authenticatorSelection.residentKey = preferred or requireResidentKey in the debugger tab.
Though our password manager app can now run smoothly when used from a browser, using it from an app is still incomplete. There are two patterns for using passkeys in Android apps - using the app's signature value as the key, or using the web domain as the key. To use the web domain as the key, it is essential to confirm that the app was developed by the owner of the web domain to prevent it from becoming a hotbed of phishing attempts. Verification of the app's signature using Digital Assets Links, which is also used for DeepLinks, is required. However, we were unable to implement this aspect within the given time frame.
Now that OS-standard passkeys are available on Mac, iOS, Android, and Windows, what are the benefits of using the Credential Provider API to allow third-party apps to manage passkeys? Firstly, it is the ability to share passkeys between different platforms. With third-party password manager apps, passkeys can now be shared between two smartphones such as Android and iOS devices or between Windows PCs and smartphones. Additionally, passkeys can be shared among family members or within a company when passkey sharing is necessary, similar to how passwords are shared now.
On the other hand, challenges are now apparent from a service providers perspective that provides login using passkeys. Until now, there has been a certain amount of security assured (implicitly) by the OS managing the passkey. However, in the future, it will be unknown who is managing the passkey. Information leakage from password managers is rare but does exist, and similar incidents happening with passkey management apps cannot be ruled out either. Users might even be tricked into using fake apps. In the event of a passkey leak, it's essential to consider how to protect the customer information and assets of those who use the service.
Still, when compared to passwords, passkeys have significant advantages, such as not being reused and not being subject to phishing attacks. With the Credential Provider API, it is expected that passkeys will become more widespread, and the security of the internet as a whole will improve.
We received advice on implementing the Credential Provider API from Adam Langley and Jerry Shi from Google, Rene Leveille from AgileBits (1Password), and Rew Islam from Dashlane within the FIDO Alliance. We'd like to thank them here.