Skip to content

Passkeys Support#775

Open
pando85 wants to merge 9 commits intoagrahn:developfrom
pando85:develop
Open

Passkeys Support#775
pando85 wants to merge 9 commits intoagrahn:developfrom
pando85:develop

Conversation

@pando85
Copy link
Copy Markdown

@pando85 pando85 commented Mar 15, 2026

Summary

This PR implements WebAuthn/Passkeys support in Android Password Store, making it compatible with the passless project. Users can now use the app as a FIDO2 authenticator for websites and services that support passkeys.

Key Features

Credential Storage (passless-compatible)

  • CBOR format: Credentials stored in CBOR binary format, fully compatible with passless
  • Filename convention: {rpId}/{credentialIdHex}.gpg (e.g., webauthn.io/a1b2c3d4....gpg)
  • Encryption: PGP-encrypted using existing crypto infrastructure
  • Field naming: snake_case for CBOR fields (sign_count, private_key, display_name)

WebAuthn Implementation

  • Algorithm: ES256 (P-256 with SHA-256)
  • Attestation: None attestation format
  • Signature format: DER-encoded ECDSA signatures
  • Client data JSON: Includes crossOrigin: false

Settings

Setting Default Description
Constant signature counter true Keep signCount at 0 for cloned authenticator detection
Auto Git sync true Automatically sync passkeys with Git remote

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Android Credential Manager                │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│               PasskeyCredentialProviderService               │
│  (handles CreateCredentialRequest, GetCredentialRequest)    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    ES256CryptoHandler                        │
│  - Key generation (P-256)                                   │
│  - Signing (DER-encoded ECDSA)                              │
│  - Attestation/Assertion building                           │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   FilePasskeyStorage                         │
│  - CBOR serialization (passless-compatible)                 │
│  - PGP encryption                                           │
│  - Index-based lookups                                      │
└─────────────────────────────────────────────────────────────┘

passless Compatibility

This implementation is fully compatible with passless, a CLI FIDO2 authenticator. Both apps can read and write the same credentials:

Aspect APS passless
Storage format CBOR CBOR
Byte array encoding Integer array (major type 4) Integer array (major type 4)
Filename {credIdHex}.gpg {credIdHex}.gpg
Encryption PGP GPG
Algorithm ES256 (-7) ES256 (-7), EdDSA (-8)

CBOR Credential Structure

{
  "id": u8 array,           // credential ID (32 bytes)
  "rp": {
    "id": "example.com",
    "name": null
  },
  "user": {
    "id": u8 array,
    "name": "username",
    "display_name": null
  },
  "sign_count": 0,            // u32
  "alg": -7,                  // ES256
  "private_key": u8 array,  // PKCS#8 encoded
  "created": 1234567890,      // Unix timestamp
  "discoverable": true,
  "extensions": {
    "cred_protect": 3,
    "hmac_secret": null
  }
}

Testing

Prerequisites

  1. Android device or emulator running API 26+
  2. PGP key configured in the app
  3. Git repository initialized

Test Registration (webauthn.io)

  1. Open Chrome and go to https://webauthn.io
  2. Enter a username (e.g., test-aps)
  3. Click "Register"
  4. When prompted, select "Password Store" from the credential provider
  5. Authenticate with biometrics/PIN if enabled
  6. Verify the credential appears in the app

Test Authentication (webauthn.io)

  1. On webauthn.io, click "Authenticate"
  2. When prompted, select "Password Store"
  3. Authenticate with biometrics/PIN
  4. Verify successful login

Test Cross-Compatibility with passless

  1. Create a credential in APS on webauthn.io
  2. Push to Git remote
  3. Pull on a machine with passless installed
  4. Run passless and authenticate to webauthn.io
  5. The credential created in APS should be usable

Test Fixture Verification

# Run CBOR compatibility tests
./gradlew :passkeys:core:test --tests "*CborTest"
./gradlew :passkeys:core:test --tests "*StoredCredentialTest"

Security Considerations

  1. Private keys: Encrypted with PGP using the existing crypto infrastructure
  2. Signatures: DER-encoded (70-72 bytes) to match WebAuthn spec
  3. Sign count: Constant by default to prevent tracking; can be disabled for cloned authenticator detection
  4. User verification: Requires biometric/PIN authentication before signing
    Future Work

Related to: #629

@agrahn
Copy link
Copy Markdown
Owner

agrahn commented Mar 17, 2026

Thank you very much! This looks like a major enhancement. Would you please try to resolve the failing checks? The one of them concerning code formatting can be fixed with ./gradlew spotlessApply. I will later look through your code and try to understand what it is doing and also run tests myself.

@pando85
Copy link
Copy Markdown
Author

pando85 commented Mar 17, 2026

Thank you for taking the time to review such a big feature! I tested in the Android emulator with webauthn.io, but I could validate in my real phone against websites that I'm currently using in Linux once you tell me that everything looks OK from code perspective.

titleRes = R.string.pref_passkey_constant_signature_counter_title
summaryRes = R.string.pref_passkey_constant_signature_counter_summary
}
switch(PreferenceKeys.PASSKEY_AUTO_GIT_SYNC) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If enabled, will this automatically trigger a Git sync whenever a new passkey has been added?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at 2c1c256

If enabled, will this automatically trigger a Git sync whenever a new passkey has been added?

Yes, that's the idea. I added to replicate passless behavior. I don't know if it worths with PASSKEY_CONSTANT_SIGNATURE_COUNTER=true.

pando85 added 4 commits March 25, 2026 00:13
…lity

Implement FIDO2/WebAuthn passkeys support allowing the app to function as a
credential provider for websites and services supporting passkeys.

Key features:
- CBOR credential storage compatible with passless
- ES256 (P-256) algorithm support
- DER-encoded ECDSA signatures
- Biometric/PIN authentication
- Settings for constant signature counter and auto git sync

Storage format:
- Credentials stored as CBOR with integer array byte encoding
- Filename: {rpId}/{credentialIdHex}.gpg
- PGP encrypted using existing crypto infrastructure

Architecture:
- PasskeyCredentialProviderService handles Android Credential Manager requests
- ES256CryptoHandler for key generation and signing
- FilePasskeyStorage with CBOR serialization
- IndexedPasskeyStorage for fast lookups

Compatible with passless (github.com/pando85/passless) for cross-platform
credential sharing via Git repository.
The setting existed in the UI but was never read by the passkey
implementation. Now when enabled (default), the signCount is kept at 0
to help detect cloned authenticators (passless compatible).
The setting existed in the UI but was never used. Now when enabled
(default), passkey creation and usage trigger a git sync operation
in the background to keep the repository in sync.

Changed AppPasskeyProviderActivity to extend BaseGitActivity to
gain access to git sync functionality.
pando85 added 2 commits March 25, 2026 00:19
- Remove duplicate updateSignCount call in AppPasskeyProviderActivity
  that was wasting I/O after assertion was already built
- Remove unused convertDerToRaw and convertRawToDer methods from
  ES256CryptoHandler that were never called
Critical fixes:
- Persist publicKey in StoredCredential CBOR serialization
  Previously publicKey was never saved, breaking passkey authentication
  after storage roundtrip

- Fix race condition in IndexedPasskeyStorage
  Add @volatile and Mutex for thread-safe index loading

- Log storage failures instead of silently swallowing them
  Change empty failure handler to proper error logging

CBOR hardening:
- Add bounds checking for integer conversions (BigInteger to Int/Long)
- Add MAX_COLLECTION_SIZE (100k) and MAX_DEPTH (100) limits
- Validate string/array lengths before converting to Int
- Add range validation for byte values in toByteArray()

Attestation improvements:
- Use credential's signCount in attestation response instead of hardcoded 0
- Add credential ID length validation (max 1023 bytes per WebAuthn spec)
- Improve require messages in CBOR encoding functions
@pando85
Copy link
Copy Markdown
Author

pando85 commented Mar 24, 2026

Thank you for this first review. I fixed both things and analyzed a bit deeper all the code in the PR.

- Add @RequiresApi(34) annotations for CredentialProviderService APIs
- Suppress deprecation warnings for Autofill APIs (deprecated in API 35)
- Suppress RawDispatchersUse lint warning (no SlackDispatchers in project)
- Remove unused kotlinx.serialization.encodeToString import
- Replace !! operator with safe null handling in tests
- Fix DER signature size range in test (68-72 bytes, not 70-72)
- Add tools:targetApi to AndroidManifest for passkey service
@pando85
Copy link
Copy Markdown
Author

pando85 commented Mar 25, 2026

I fixed CI failures

pando85 added 2 commits March 26, 2026 18:34
- Disable InvalidPackage check for third-party libraries
- Disable RawDispatchersUse check for Android modules
- Add empty baseline files for passkeys modules
@pando85
Copy link
Copy Markdown
Author

pando85 commented Mar 26, 2026

I guess that this could be the final commit to fix CI. I don't receive any notifications when CI fails, so I'm trying to check this from time to time.

@pando85
Copy link
Copy Markdown
Author

pando85 commented Mar 29, 2026

Everything green, ready to review again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants