Important! Before you proceed, please read the EUDI Wallet Reference Implementation project description
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
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
- Support for OpenId4VCI Draft 14 document issuance
- 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
- Support for ISO-18013-5 device retrieval
- Remote document presentation
- OpenId4VP document transfer
- For pre-registered verifiers
- Dynamic registration of verifiers
- OpenId4VP document transfer
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.
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
- Android 8 (API level 26) or higher
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.3"
// 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"
}
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' keysAndroidStorageEngine
for storing the documents' dataReaderTrustStore
implementation for validating the reader's certificatesPresentationManager
implementation for managing both proximity and remote presentation of documentsLogger
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.
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
}
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
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
}
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.
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 ->
// ...
}
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:
- Using the
OpenId4VciManager.issueDocumentByDocType
method, when the document's docType is known. - Using the
OpenId4VciManager.issueDocumentByOffer
orOpenId4VciManager.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 ->
// ...
}
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) }
}
// ...
}
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.
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
}
}
}
The library supports the following 3 ways to transfer documents:
- Offline document transfer between devices over BLE, according to the ISO 18013-5 specification
- Device engagement using QR code
- NFC device engagement
- Document retrieval to a website according to the ISO 18013-7 specification
- RestAPI using app link
- 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:
TransferEvent.QrEngagementReady
: The QR code is ready to be displayed. Get the QR code fromevent.qrCode
.TransferEvent.Connecting
: The devices are connecting. Use this event to display a progress indicator.TransferEvent.Connected
: The devices are connected.TransferEvent.RequestReceived
: A request is received. Get the parsed request fromevent.requestedDocumentData
and the initial request as received by the verifier fromevent.request
.TransferEvent.ResponseSent
: A response is sent.TransferEvent.Redirect
: This event prompts to redirect the user to the given Redirect URI. Get the Redirect URI fromevent.redirectUri
. This event maybe be returned when OpenId4Vp is used as a transmission channel.TransferEvent.Disconnected
: The devices are disconnected.TransferEvent.Error
: An error occurred. Get theThrowable
error fromevent.error
.
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
// This is triggered when Relaying Party (RP) has accepted the response and
// the RP is redirecting the user to the given redirect URI
// If this event is triggered, then the TransferEvent.ResponseSent event will not be triggered
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()
}
}
}
-
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 -> {} }
-
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)
andwallet.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) } }
-
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!!) } }
-
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) } }
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.
We welcome contributions to this project. To ensure that the process is smooth for everyone involved, follow the guidelines found in CONTRIBUTING.md.
See licenses.md for 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.