Skip to content

eu-digital-identity-wallet/eudi-lib-android-wallet-core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

EUDI Wallet Core library for Android

Important! Before you proceed, please read the EUDI Wallet Reference Implementation project description

Overview

This repository contains the EUDI Wallet Core library for Android. The library is a part of the EUDI Wallet Reference Implementation project.

This library acts as a coordinator by orchestrating the various components that are required to implement the EUDI Wallet functionality. On top of that, it provides a simplified API that can be used by the application to implement the EUDI Wallet functionality.

graph TD
;
    A[eudi-lib-android-wallet-core]
    B[eudi-lib-android-wallet-document-manager] -->|DocumentManager| A
    C[eudi-lib-android-iso18013-data-transfer] -->|TransferManager| A
    D[eudi-lib-jvm-openid4vci-kt] -->|OpenId4VciManager| A
    E[eudi-lib-jvm-siop-openid4vp-kt] -->|OpenId4VpManager| A
    F[com.android.identity] -->|SecureArea,StorageEngine| B
    H[eudi-lib-jvm-presentation-exchange] --> E
    G[identity-credential-android] --> A
    B -->|DocumentManager| C
    F -->|SecureArea,StorageEngine| A
Loading

The library provides the following functionality:

  • Document management
    • Documents' Key creation and management with Android Keystore by default
    • Support for custom SecureArea implementations
    • Support for multiple SecureArea implementations
  • Document issuance
    • Support for OpenId4VCI Draft 14 document issuance
      • Authorization Code Flow
      • Pre-authorization Code Flow
      • Support for mso_mdoc format
      • Support for sd-jwt-vc format
      • Support credential offer
      • Support for DPoP JWT in authorization
      • Support for JWT proof types
      • Support for deferred issuing
  • Proximity document presentation
    • Support for ISO-18013-5 device retrieval
      • QR device engagement
      • NFC device engagement
      • BLE data transfer
      • NFC data transfer
      • Wifi-Aware data transfer
  • Remote document presentation
    • OpenId4VP document transfer
      • For pre-registered verifiers
      • Dynamic registration of verifiers

The library is written in Kotlin and is compatible with Java. It is distributed as a Maven package and can be included in any Android project that uses Android 8 (API level 26) or higher.

Disclaimer

The released software is a initial development release version:

  • The initial development release is an early endeavor reflecting the efforts of a short timeboxed period, and by no means can be considered as the final product.
  • The initial development release may be changed substantially over time, might introduce new features but also may change or remove existing ones, potentially breaking compatibility with your existing code.
  • The initial development release is limited in functional scope.
  • The initial development release may contain errors or design flaws and other problems that could cause system or other failures and data loss.
  • The initial development release has reduced security, privacy, availability, and reliability standards relative to future releases. This could make the software slower, less reliable, or more vulnerable to attacks than mature software.
  • The initial development release is not yet comprehensively documented.
  • Users of the software must perform sufficient engineering and additional testing in order to properly evaluate their application and determine whether any of the open-sourced components is suitable for use in that application.
  • We strongly recommend not putting this version of the software into production use.
  • Only the latest version of the software will be supported

Requirements

  • Android 8 (API level 26) or higher

Dependencies

To use snapshot versions add the following to your project's settings.gradle file:

dependencyResolutionManagement {
    repositories {
        // ...
        maven {
            url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
            mavenContent { snapshotsOnly() }
        }
        // ...
    }
}

To include the library in your project, add the following dependencies to your app's build.gradle file.

dependencies {
    implementation "eu.europa.ec.eudi:eudi-lib-android-wallet-core:0.12.1"
    // required when using the built-in AndroidKeystoreSecureArea implementation provided by the library
    // for user authentication with biometrics
    implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
}

How to Use

Initialize the library

To instantiate a EudiWallet use the EudiWallet.Builder class or the EudiWallet.invoke method, from the EudiWallet companion object.

The minimum requirements to initialize the library is to provide a EudiWalletConfit object that will be used to configure the library's built-in components.

The built-in components are:

  • AndroidKeystoreSecureArea for storing and managing the documents' keys
  • AndroidStorageEngine for storing the documents' data
  • ReaderTrustStore implementation for validating the reader's certificates
  • PresentationManager implementation for managing both proximity and remote presentation of documents
  • Logger implementation for logging

The following example demonstrates how to initialize the library for using the built-in components:

// configuring the wallet
val config = EudiWalletConfig()
    // configure the document storage
    // the noBackupFilesDir is used to store the documents by default
    .configureDocumentManager(context.noBackupFilesDir)
    // configure the built-in logger
    .configureLogging(
        // set log level to info
        level = Logger.LEVEL_INFO
    )
    // configure the built-in key creation settings
    .configureDocumentKeyCreation(
        // set userAuthenticationRequired to true to require user authentication
        userAuthenticationRequired = true,
        // set userAuthenticationTimeout to 30 seconds
        userAuthenticationTimeout = 30_000L,
        // set useStrongBoxForKeys to true to use the the device's StrongBox if available
        // to store the keys
        useStrongBoxForKeys = true
    )
    .configureReaderTrustStore(
        // set the reader trusted certificates for the reader trust store
        listOf(readerCertificate)
    )
    // mandatory configuration for OpenId4Vci if you want to issue documents
    .configureOpenId4Vci {
        withIssuerUrl("https://issuer.com")
        withClientId("client-id")
        withAuthFlowRedirectionURI("eudi-openid4ci://authorize")
    }
    // configuration for proximity presentation
    // the values below are the default values
    .configureProximityPresentation(
        // ble mode: peripheral and/or central
        enableBlePeripheralMode = true,
        enableBleCentralMode = false,
        clearBleCache = true,
        // regitered application service for handling NFC device engagement
        nfcEngagementServiceClass = MyNfcEngagementService::class.java
    )
    // mandatory configuration for OpenId4Vp if you want to enable
    // remote presentation of documents with OpenId4Vp
    .configureOpenId4Vp {
        withEncryptionAlgorithms(
            EncryptionAlgorithm.ECDH_ES
        )
        withEncryptionMethods(
            EncryptionMethod.A128CBC_HS256,
            EncryptionMethod.A256GCM
        )
        withClientIdSchemes(
            ClientIdScheme.X509SanDns
        )
        withSchemes(
            "openid4vp",
            "eudi-openid4vp",
            "mdoc-openid4vp"
        )
    }

val wallet = EudiWallet(context, config)

EuidWallet.Builder allows to configure the library with custom implementations of the built-in components.

The following example demonstrates how to initialize the library with custom implementations for StorageEngine, SecureArea, ReaderTrustStore, and Logger:

val wallet = EudiWallet(context, config) {
    // custom StorageEngine to store documents' data
    withStorageEngine(myStorageEngine)
    // a list of SecureArea implementations to be used
    withSecureAreas(listOf(deviceSecureArea, cloudSecureArea))
    // ReaderTrustStore to be used for reader authentication
    withReaderTrustStore(myReaderTrustStore)
    // custom logger to be used
    withLogger(myLogger)
}

See the CustomizeSecureArea.md for more information on how to use the wallet-core library with custom SecureArea implementations.

Manage documents

The library provides a set of methods to work with documents.

val documents = wallet.getDocuments()

You can also retrieve documents based on a predicate. The following snippet shows how to retrieve documents of mso_mdoc format of a specific docType:

val documents = wallet.getDocuments { document ->
    (document.format as MsoMdocFormat).docType == "eu.europa.ec.eudi.pid.1"
}

The following snippet shows how to retrieve a document by its id:

val documentId = "some_document_id"
val document: Document? = wallet.getDocumentById(documentId)

To delete a document, use the following code snippet:

try {
    val documentId = "some_document_id"
    val deleteResult = wallet.deleteDocumentById(documentId)
    deleteResult.getOrThrow()
} catch (e: Throwable) {
    // Handle the exception
}

Creating and storing a new document

Adding a new document to the wallet is a two-step process. First, a new document must be created using the createDocument method. The method returns an UnsignedDocument object that contains the keys that will be used for signing the proof of possession for the issuer. Creating a new document requires the document format and the create key settings. The create key settings can be used to specify the way the keys are created.

When using the built-in AndroidKeystoreSecureArea implementation of the library, the wallet.getDefaultCreateDocumentSettings() extension can be used to create an instance of the appropriate CreateDocumentSettings class.

After the document is created, the user must retrieve the document's data from the issuer and store it in the wallet using the storeIssuedDocument method.

The following snippet demonstrates how to create a new document for the mso_mdoc format, using library's default implementation of CreateDocumentSettings.

try {
    // create a new document
    // Construct the CreateDocumentSettings object
    val createSettings = wallet.getDefaultCreateDocumentSettings()

    val createDocumentResult = wallet.createDocument(
        format = MsoMdocFormat(docType = "eu.europa.ec.eudi.pid.1"),
        createSettings = createSettings
    )
    val unsignedDocument = createDocumentResult.getOrThrow()
    val publicKeyBytes = unsignedDocument.publicKeyCoseBytes

    // prepare keyUnlockData to unlock the key
    // here we use the default key unlock data for the document
    // provided by the library
    val keyUnlockData = unsignedDocument.DefaultKeyUnlockData
    // proof of key possession
    // Sign the documents public key with the private key
    // before sending it to the issuer
    val signatureResult =
        unsignedDocument.sign(publicKeyBytes, keyUnlockData = keyUnlockData)
    val signature = signatureResult.getOrThrow().toCoseEncoded()

    // send the public key and the signature to the issuer
    // and get the document data
    val documentData = sendToIssuer(
        publicKeyCoseBytes = publicKeyBytes,
        signatureCoseBytes = signature
    )

    // store the issued document with the document data received from the issuer
    val storeResult =
        wallet.storeIssuedDocument(unsignedDocument, documentData)

    // get the issued document
    val issuedDocument = storeResult.getOrThrow()
} catch (e: Throwable) {
    // Handle the exception
}

// ...

fun sendToIssuer(publicKeyCoseBytes: ByteArray, signatureCoseBytes: ByteArray): ByteArray {
    TODO("Send publicKey and proof of possession signature to issuer and retrieve document's data")
}

Important!: In the case of DocumentFormat.MsoMdoc, DocumentManager.storeIssuedDocument() method expects document's data to be in CBOR bytes and have the IssuerSigned structure according to ISO 23220-4.

Currently, the library does not support IssuerSigned structure without the nameSpaces field.

The following CDDL schema describes the structure of the IssuerSigned structure:

IssuerSigned = {
 ?"nameSpaces" : IssuerNameSpaces, ; Returned data elements
 "issuerAuth" : IssuerAuth ; Contains the mobile security object (MSO) for issuer data authentication
}
IssuerNameSpaces = { ; Returned data elements for each namespace
 + NameSpace => [ + IssuerSignedItemBytes ]
}
IssuerSignedItemBytes = #6.24(bstr .cbor IssuerSignedItem)
IssuerSignedItem = {
 "digestID" : uint, ; Digest ID for issuer data authentication
 "random" : bstr, ; Random value for issuer data authentication
 "elementIdentifier" : DataElementIdentifier, ; Data element identifier
 "elementValue" : DataElementValue ; Data element value
}
IssuerAuth = COSE_Sign1 ; The payload is MobileSecurityObjectBytes

Working with sample documents

The wallet-core library provides a method to load sample documents easily. This feature is useful for demonstration or testing purposes.

Currently, the library supports loading sample documents in MsoMdoc format.

The following code snippet shows how to load sample documents:

val sampleMdocDocuments: ByteArray = readFileWithSampleData()

val createSettings = wallet.getDefaultCreateDocumentSettings()
val loadResult = wallet.loadMdocSampleDocuments(
    sampleData = sampleMdocDocuments,
    createSettings = createSettings,
    documentNamesMap = mapOf(
        "eu.europa.ec.eudi.pid.1" to "EU PID",
        "org.iso.18013.5.1.mDL" to "mDL"
    )
)

val documentIds: List<DocumentId> = loadResult.getOrThrow()

// ...

fun readFileWithSampleData(): ByteArray = TODO("Reads the bytes from file with sample documents")

Sample documents must be in CBOR format with the following structure:

Data = {
 "documents" : [+Document] ; Sample documents
}
Document = {
 "docType" : DocType, ; Document type returned
 "issuerSigned" : IssuerSigned ; Data elements
}
IssuerSigned = {
 "nameSpaces" : IssuerNameSpaces, ; Returned data elements
}
IssuerNameSpaces = { ; Returned data elements for each namespace
 + NameSpace => [ + IssuerSignedItemBytes ]
}
IssuerSignedItem = {
 "digestID" : uint, ; Digest ID for issuer data authentication
 "random" : bstr, ; Random value for issuer data authentication
 "elementIdentifier" : DataElementIdentifier, ; Data element identifier
 "elementValue" : DataElementValue ; Data element value
}

Issue document using OpenID4VCI

The library provides issuing documents using OpenID4VCI protocol. To issue a document using this functionality, EudiWallet must be initialized with the openId4VciConfig configuration, during configuration. See the Initialize the library section.

Resolving Credential offer

First, you need an instance of the OpenId4VciManager class. You can create an instance of the class by calling the EudiWallet.createOpenId4VciManager method.

The library provides the OpenId4VciManager.resolveDocumentOffer method that resolves the credential offer URI. The method returns the resolved Offer object that contains the offer's data. The offer's data can be displayed to the user.

The following example shows how to resolve a credential offer:

val offerUri = "https://issuer.com/?credential_offer=..."
// Create an instance of OpenId4VciManager
val openId4VciManager = wallet.createOpenId4VciManager()
openId4VciManager.resolveDocumentOffer(offerUri) { result ->

    when (result) {
        is OfferResult.Success -> {
            val offer: Offer = result.offer
            // display the offer's data to the user
            val issuerName = offer.issuerName
            val offeredDocuments: List<OfferedDocument> = offer.offeredDocuments
            val txCodeSpec: Offer.TxCodeSpec? =
                offer.txCodeSpec // information about pre-authorized flow
            // ...
        }
        is OfferResult.Failure -> {
            val error = result.cause
            // handle error while resolving the offer
        }
    }
}

There is also the availability for the OpenId4VciManager.resolveDocumentOffer method to specify the executor in which the onResolvedOffer callback is executed, by assigning the executor parameter. If the executor parameter is null, the callback will be executed on the main thread.

val executor = Executors.newSingleThreadExecutor()
openId4VciManager.resolveDocumentOffer(offerUri, executor) { result ->
    // ...
}

Issuing a document

First, you need an instance of the OpenId4VciManager class. You can create an instance of the class by calling the EudiWallet.createOpenId4VciManager method.

There are two ways to issue a document using OpenID4VCI:

  1. Using the OpenId4VciManager.issueDocumentByDocType method, when the document's docType is known.
  2. Using the OpenId4VciManager.issueDocumentByOffer or OpenId4VciManager.issueDocumentByOfferUri methods, when an OpenId4VCI offer is given.

Important note:

  • Currently, only mso_mdoc format is supported
  • Currently, only the ES256 algorithm is supported for signing OpenId4CVI proof of possession of the publicKey.
  • See the CustomizeSecureArea.md for more information on how to use the wallet-core library and OpenId4VCI with custom SecureArea implementations.

The following example shows how to issue a document using OpenID4VCI:

val onIssueEvent = OnIssueEvent { event ->
    when (event) {
        is IssueEvent.Started -> {
            // indicates that OpenId4VCI process is started
            // and holds the total number of documents to be issued
            val numberOfDocumentsToBeIssued: Int = event.total
        }

        is IssueEvent.Finished -> {
            // triggered when the OpenId4VCI process is finished
            // and holds the documentIds of the issued documents
            val issuedDocumentIds: List<String> = event.issuedDocuments
        }

        is IssueEvent.Failure -> {
            // triggered when an error occurs during the OpenId4VCI process
            // and holds the error
            val cause = event.cause
        }

        is IssueEvent.DocumentIssued -> {
            // triggered each time a document is issued
            // and holds information about the issued document
            val documentId: String = event.documentId
            val documentName: String = event.name
            val docType: String = event.docType
        }

        is IssueEvent.DocumentFailed -> {
            // triggered when an error occurs during the issuance of a document
            // and holds the error and information about the failed to issue document
            val documentName: String = event.name
            val docType: String = event.docType
            val cause: Throwable = event.cause
        }

        is IssueEvent.DocumentRequiresUserAuth -> {
            // triggered when user authentication is required to issue a document
            // Holds the document object that requires user authentication
            // and the algorithm that is going to be used for signing the proof of possession,
            // as well as methods for resuming the issuance process or canceling it
            val signingAlgorithm = event.signingAlgorithm
            val document = event.document
            // create the keyUnlockData to unlock the key. Here we use the default key unlock data
            // provided by the library
            val keyUnlockData = document.DefaultKeyUnlockData
            // use the keyUnlockData to get the crypto object for authenticating
            // the user using bio-metrics or any other method
            val cryptoObject = keyUnlockData.getCryptoObjectForSigning(signingAlgorithm)

            // to resume the issuance process, after authenticating user,  call
            // resume with the keyUnlockData
            event.resume(keyUnlockData)

            // or cancel the issuance process by calling cancel method
            event.cancel("User canceled the issuance process")
        }

        is IssueEvent.DocumentDeferred -> {
            // triggered when the document issuance is deferred
            // and holds the documentId of the deferred document
            val documentId: String = event.documentId
            val documentName: String = event.name
            val docType: String = event.docType
        }

        is IssueEvent.DocumentRequiresCreateSettings -> {
            // triggered when creating a document for a given offered document
            val offeredDocument = event.offeredDocument
            // create the createSettings for the document
            // Here we use the default create settings provided by the library
            val createSettings = wallet.getDefaultCreateDocumentSettings()
            // resume the issuance process with the createSettings
            event.resume(createSettings)
            // or cancel the issuance process
            event.cancel("User canceled the issuance process")
        }
    }
}
// Create an instance of OpenId4VciManager
val openId4VciManager = wallet.createOpenId4VciManager()

openId4VciManager.issueDocumentByDocType(
    docType = "eu.europa.ec.eudi.pid.1",
    txCode = "<Transaction Code for Pre-authorized flow>", // if transaction code is provided
    onIssueEvent = onIssueEvent
)
// or
openId4VciManager.issueDocumentByOfferUri(
    offerUri = "https://issuer.com/?credential_offer=...",
    txCode = "<Transaction Code for Pre-authorized flow>", // if transaction code is provided
    onIssueEvent = onIssueEvent
)
// or given a resolved offer object
openId4VciManager.issueDocumentByOffer(
    offer = offer,
    txCode = "<Transaction Code for Pre-authorized flow>", // if transaction code is provided
    onIssueEvent = onIssueEvent
)

There's also available for issueDocumentByDocType, issueDocumentByOfferUri and issueDocumentByOffer methods to specify the executor in which the onIssueEvent callback is executed, by assigning the executor parameter. If the executor parameter is null, the callback will be executed on the main thread.

// Create an instance of OpenId4VciManager
val openId4VciManager = wallet.createOpenId4VciManager()

val executor = Executors.newSingleThreadExecutor()
openId4VciManager.issueDocumentByDocType(
    docType = "eu.europa.ec.eudi.pid.1",
    executor = executor
) { event ->
    // ...
}

Authorization code flow

For the authorization code flow to work, the application must handle the redirect URI. The redirect URI is the URI that the Issuer will redirect the user to after the user has authenticated and authorized. The redirect URI must be handled by the application and resume the issuance process by calling the OpenId4VciManager.resumeWithAuthorization. Also, the redirect uri declared in the OpenId4VCI configuration must be declared in the application's manifest file.

Important note: The resumeWithAuthorization method must be called from the same OpenId4VciManager instance that was used to start the issuance process. You will need to keep the reference of the OpenId4VciManager instance that was used for calling the issueDocumentByDocType, issueDocumentByOfferUri or issueDocumentByOffer method and use this same instance to call the resumeWithAuthorization method.

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <!-- rest of manifest -->
        <activity android:name=".MainActivity" android:exported="true">
            <!-- rest of activity -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data android:scheme="eudi-openid4ci" android:host="authorize" />
            </intent-filter>
        </activity>
    </application>
</manifest>
 // ...
EudiWalletConfig()
    // ... 
    .configureOpenId4Vci {
        // ...
        withAuthFlowRedirectionURI("eudi-openid4ci://authorize")
        // ...
    }
//...
import javax.management.openmbean.OpenMBeanInfo

class SomeActivity : AppCompatActivity() {

    val openId4VciManager: OpenId4VciManager
        get() {
            // get the OpenId4VciManager instance that was created during the issuance process
            // ...
        }

    // ...
    override fun onResume() {
        super.onResume()
        // check if intent is from the redirect uri to resume the issuance process
        // ...
        // then call
        intent.data?.let { uri -> openId4VciManager.resumeWithAuthorization(uri) }
    }
    // ...
}

Pre-Authorization code flow

When Issuer supports the pre-authorization code flow, the resolved offer will also contain the corresponding information. Specifically, the txCodeSpec field in the Offer object will contain:

  • The input mode, whether it is NUMERIC or TEXT
  • The expected length of the input
  • The description of the input

From the user's perspective, the application must provide a way to input the transaction code.

When the transaction code is provided, the issuance process can be resumed by calling any of the following methods:

  • EudiWallet.issueDocumentByDocType
  • EudiWallet.issueDocumentByOfferUri
  • EudiWallet.issueDocumentByOffer

passing the transaction code as in the txCode parameter.

Deferred Issuance

When the document issuance is deferred, the IssueEvent.DocumentDeferred event is triggered. The deferred document can be issued later by calling the OpenId4VciManager.issueDeferredDocument method.

// given a deferred document, for example:
val deferredDocumentId: DocumentId = "deferred-document-id"
val deferredDocument = wallet.getDocumentById(deferredDocumentId) as DeferredDocument

val openId4VciManager: OpenId4VciManager = wallet.createOpenId4VciManager()

openId4VciManager.issueDeferredDocument(deferredDocument) { result ->
    when (result) {
        is DeferredIssueResult.DocumentIssued -> {
            // document issued
        }
        is DeferredIssueResult.DocumentFailed -> {
            // error
            val cause = result.cause
        }
        is DeferredIssueResult.DocumentNotReady -> {
            // The document is not issued yet
        }
        is DeferredIssueResult.DocumentExpired -> {
            // The document is expired and cannot be issued
        }
    }
}

Transfer documents

The library supports the following 3 ways to transfer documents:

  1. Offline document transfer between devices over BLE, according to the ISO 18013-5 specification
    • Device engagement using QR code
    • NFC device engagement
  2. Document retrieval to a website according to the ISO 18013-7 specification
    • RestAPI using app link
  3. Document retrieval using OpenID4VP

Transfer process is asynchronous. During the transfer, events are emitted that indicate the current state of the transfer. The following events are emitted:

  1. TransferEvent.QrEngagementReady: The QR code is ready to be displayed. Get the QR code from event.qrCode.
  2. TransferEvent.Connecting: The devices are connecting. Use this event to display a progress indicator.
  3. TransferEvent.Connected: The devices are connected.
  4. TransferEvent.RequestReceived: A request is received. Get the parsed request from event.requestedDocumentData and the initial request as received by the verifier from event.request.
  5. TransferEvent.ResponseSent: A response is sent.
  6. TransferEvent.Redirect: This event prompts to redirect the user to the given Redirect URI. Get the Redirect URI from event.redirectUri. This event maybe be returned when OpenId4Vp is used as a transmission channel.
  7. TransferEvent.Disconnected: The devices are disconnected.
  8. TransferEvent.Error: An error occurred. Get the Throwable error from event.error.

Attaching a TransferEvent.Listener

To receive events from the EudiWallet, you must attach a TransferEvent.Listener to it:

The following example demonstrates how to implement a TransferEvent.Listener and attach it to the EudiWallet object.

wallet.addTransferEventListener { event ->
    when (event) {
        is TransferEvent.QrEngagementReady -> {
            // Qr code is ready to be displayed
            val qrCodeBitmap = event.qrCode.asBitmap(size = 800)
            // or
            val qrCodeView = event.qrCode.asView(context, size = 800)
        }

        TransferEvent.Connecting -> {
            // Informational event that devices are connecting
        }

        TransferEvent.Connected -> {
            // Informational event that the transfer has been connected
        }

        is TransferEvent.RequestReceived -> try {
            // get the processed request
            val processedRequest = event.processedRequest.getOrThrow()
            // the request has been received and processed

            // the request processing was successful
            // requested documents can be shown in the application
            val requestedDocuments = processedRequest.requestedDocuments
            // ...
            // application must create the DisclosedDocuments object
            val disclosedDocuments = DisclosedDocuments(
                DisclosedDocument(
                    documentId = "document-id",
                    disclosedItems = listOf(
                        DocItem(
                            namespace = "eu.europa.ec.eudi.pid.1",
                            elementIdentifier = "first_name"
                        ),
                    ),
                    // keyUnlockData is required if needed to unlock the key
                    // in order to sign the response
                    keyUnlockData = wallet.getDefaultKeyUnlockData("document-id")
                ),
                // ... rest of the disclosed documents
            )
            // generate the response
            val response = processedRequest.generateResponse(
                disclosedDocuments = disclosedDocuments,
                signatureAlgorithm = Algorithm.ES256
            ).getOrThrow()

            wallet.sendResponse(response)

        } catch (e: Throwable) {
            // An error occurred
            // handle the error
        }

        TransferEvent.ResponseSent -> {
            // Informational event that the response has been sent
        }

        is TransferEvent.Redirect -> {
            // A redirect is needed. Used mainly for the OpenId4VP implementation
            val redirectUri = event.redirectUri // the redirect URI
        }

        TransferEvent.Disconnected -> {
            // Informational event that device has been disconnected
            // stop the proximity presentation
            wallet.stopProximityPresentation()
        }

        is TransferEvent.Error -> {
            // An error occurred
            val cause = event.error
            // stop the proximity presentation
            wallet.stopProximityPresentation()
        }
    }
}

Initiating transfer

  1. BLE transfer using QR Engagement

    Once a transfer event listener is attached, use the EudiWallet.startProximityPresentation() method to start the QR code engagement.

    wallet.startProximityPresentation()
    
    //... other code
    
    // in event listener when the qr code is ready to be displayed
    when (event) {
        is TransferEvent.QrEngagementReady -> {
            // show the qr code to the user
            val qrCode: QrCode = event.qrCode
            val qrBitmap = qrCode.asBitmap(size = 512) // get the qr code as bitmap
            // - or -
            val qrView = qrCode.asView(context, size = 512) // get the qr code as view
        }
        // ... rest of the event types
        else -> {}
    }
  2. BLE transfer using NFC Engagement

    To use NFC for engagement, you must implement a service that extends the abstract class NfcEngagementService and register it in your application's manifest file, like shown below:

    <application>
        <!-- rest of manifest -->
        <service android:exported="true" android:label="@string/nfc_engagement_service_desc"
                android:name="com.example.app.MyNfcEngagementService"
                android:permission="android.permission.BIND_NFC_SERVICE">
            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
            </intent-filter>
    
            <!-- the following "@xml/nfc_engagement_apdu_service" in meta-data is provided by the library -->
            <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
                    android:resource="@xml/nfc_engagement_apdu_service" />
        </service>
    
    </application>

    Then the service class must be also declared during wallet configuration using the EudiWalletConfig.configureProximityPresentation method. For example see Initialize the library section.

    In your application you can enable or disable the NFC engagement in your app by calling the wallet.enableNFCEngagement(ComponentActivity) and wallet.disableNFCEngagement(ComponentActivity) methods.

    In the example below, the NFC engagement is enabled when activity is resumed and disabled when the activity is paused.

    import androidx.appcompat.app.AppCompatActivity
    
    class MainActivity : AppCompatActivity() {
        
        lateinit var wallet: EudiWallet
    
        override fun onResume() {
            super.onResume()
            wallet.enableNFCEngagement(this)
        }
    
        override fun onPause() {
            super.onPause()
            wallet.disableNFCEngagement(this)
        }
    }
  3. RestAPI using app link

    To enable ISO 18013-7 REST API functionality, declare to your app's manifest file (AndroidManifest.xml) an Intent Filter for your MainActivity:

    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="mdoc" android:host="*" />
    </intent-filter>

    and set launchMode="singleTask" for this activity.

    To initiate the transfer using an app link (reverse engagement), use the wallet.startRemotePresentation(Uri) method. See the example below:

    class MainActivity : AppCompatActivity() {
    
        lateinit var wallet: EudiWallet
        // ... rest of activity code
    
        override fun onResume() {
            super.onResume()
            wallet.startRemotePresentation(intent.data!!)
        }
    
        override fun onNewIntent(intent: Intent) {
            super.onNewIntent(intent)
            wallet.startRemotePresentation(intent.data!!)
        }
    }
  4. OpenID4VP

    To use the OpenID4VP functionality, the configuration that is used to initialize the library must contain the openId4VpConfig. See the Initialize the library section.

    Then, declare to your app's manifest file (AndroidManifest.xml) the following Intent Filters for your MainActivity, for the scheme:

    <intent-filter>
       <action android:name="android.intent.action.VIEW" />
       <category android:name="android.intent.category.DEFAULT" />
       <category android:name="android.intent.category.BROWSABLE" />
       <data android:scheme="mdoc-openid4vp" android:host="*" />
    </intent-filter> 

    Also set launchMode="singleTask" for this activity.

    class MainActivity : AppCompatActivity() {
        
        lateinit var wallet: EudiWallet
        // ... rest of activity code
    
    
        override fun onResume() {
            super.onResume()
            handleOpenId4VpIntent(intent)
        }
    
        override fun onNewIntent(intent: Intent) {
            super.onNewIntent(intent)
            setIntent(null)
            handleOpenId4VpIntent(intent)
        }
    
        private fun handleOpenId4VpIntent(intent: Intent) {
            val uri = intent.data!!
            wallet.startRemotePresentation(uri)
        }
    }

Receiving a request and sending a response

When a TransferEvent.RequestReceived event is triggered, the processed request can be obtained by calling event.processedRequest. If the request is successfully processed, the requested documents are extracted from the processedRequest object.

The application then show the requested documents to the user and later create a DisclosedDocuments object, which includes the documents to be disclosed in the response. Each DisclosedDocument must contain the documentId of the disclosed document, a list of DocItem objects representing the disclosed items, and keyUnlockData if needed to unlock the key for signing the response.

After creating the DisclosedDocuments object, a response can be generated using the processedRequest.generateResponse method, specifying the disclosed documents and the signature algorithm (Algorithm.ES256). The generated response is then sent using the wallet.sendResponse method.

val transferEventListener = TransferEvent.Listener { event ->
    when (event) {

        is TransferEvent.RequestReceived -> try {
            // get the processed request
            val processedRequest = event.processedRequest.getOrThrow()
            // the request has been received and processed

            // the request processing was successful
            // requested documents can be shown in the application
            val requestedDocuments = processedRequest.requestedDocuments
            // ...
            // application must create the DisclosedDocuments object
            // Here for simplicity we assume that the first document is the only requested document
            // and we disclose only the first name

            // get the first document by id
            val firstDocumentId = requestedDocuments.first().documentId

            val firstDocument = wallet.getDocumentById(firstDocumentId) as IssuedDocument
            // We also assume that it requires user authentication
            // so we create the keyUnlockData to unlock the key
            val keyUnlockData = firstDocument.DefaultKeyUnlockData
            val cryptoObject = keyUnlockData.getCryptoObjectForSigning(Algorithm.ES256)
            // authenticate the user using the cryptoObject
            // ...

            val disclosedDocuments = DisclosedDocuments(
                DisclosedDocument(
                    documentId = firstDocumentId,
                    disclosedItems = listOf(
                        DocItem(
                            namespace = "eu.europa.ec.eudi.pid.1",
                            elementIdentifier = "first_name"
                        ),
                    ),
                    // keyUnlockData is required if needed to unlock the key
                    // in order to sign the response
                    keyUnlockData = keyUnlockData
                ),
                // ... rest of the disclosed documents
            )
            // generate the response
            val response = processedRequest.generateResponse(
                disclosedDocuments = disclosedDocuments,
                signatureAlgorithm = Algorithm.ES256
            ).getOrThrow()

            wallet.sendResponse(response)

        } catch (e: Throwable) {
            // An error occurred
            // handle the error
        }
        // handle other events
        else -> {}
    }
}

See also, the CustomizeSecureArea.md for more information on how to use the wallet-core library for presentation with custom SecureArea implementations.

How to contribute

We welcome contributions to this project. To ensure that the process is smooth for everyone involved, follow the guidelines found in CONTRIBUTING.md.

License

Third-party component licenses

See licenses.md for details.

License details

Copyright (c) 2023 European Commission

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.