How we successfully implemented the Unified Biometric System in a Flutter app
Hello everyone! My name is Vadim, I am a senior Flutter developer at STM Labs. In this article, I want to share our experience of implementing a unified biometric system in an application written in Flutter.
Why is a unified biometric system needed?
The Unified Biometric System (UBS) is a state digital platform that allows identifying a person by biometric characteristics: voice and face.
Identification in such a system gives the user the opportunity to receive commercial and government services that require identification using a passport.
Isn't it easier to use standard methods for biometric authentication?
Of course it is simpler. But biometric data stored on the device does not allow to identify a specific person to receive state or commercial services on the territory of the Russian Federation (local_auth is not suitable for this task).
Features of integrating EBS into a mobile application
Data transmission protection
To ensure the security of work with biometric samples (BS), in accordance with the law, at all stages it is necessary to ensure data encryption using the GOST algorithm – including the creation of a secure GOST TLS communication channel between the MP and the server part. In our case, it is provided by a specialized additional adapter ESIA/EBS and CryptoPro NGate.
Lack of open API
EBS does not disclose its internal API for mobile applications, so we cannot operate with all the data and methods. We only have an entry point and an exit point. At this stage, the question arises: what to integrate then? The answer is simple – we will integrate an adapter for working with the system client.
Need your own test/industrial stand
If you assumed that the result of authentication is directly personal data, then this is not quite true. We only receive a secret key, which is then used on our backend to extract personal data.
Part 1. Statement of the problem
First, it’s worth understanding what it is EBS adapter for a mobile application? This is a library that provides:
Checking whether the State Services Biometrics application is installed on the user’s device.
Interaction between the client application and the State Services Biometrics application.
That is, we must link our application with the State Services Biometrics application. The adapter allows you to do this using Intent in Android and URL Scheme in iOS.
Next, we will decide on the method of embedding into the application. We settled on developing our own plugin for interaction with host platforms using a code generator Pigeon.
The integration scheme is based on calling EBS methods through a plugin and receiving the result in the method's return data.
So we must:
Create a plugin and implement it Pigeon;
Embed the library into the plugin EBS Adapter;
Embed the plugin into a mobile application.
Part 2. Creating a plugin and connecting pigeon
Create an empty plugin and in dependencies dev_dependencies
we indicate pigeon
.
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: ^2.4.8
pigeon: ^17.2.0
Add to folder lib
new file – native_api.dart
and create an abstract class that will represent the interface for interacting with native code. This abstract class is annotated @HostApi()
signaling to Pigeon that it is intended for use on the host platform. Inside this class, we will define methods that we will later pass to the native side.
@HostApi()
abstract class NativeHostApi {}
Additionally, we will add an annotation @ConfigurePigeon()
which will define the basic configuration of the generator and the storage location of the generated files.
@ConfigurePigeon(
PigeonOptions(
// Например: lib/native_api.g.dart
input: '<путь к native_api.dart>',
dartOut: '<путь к сгенерированному lib/native_api.g.dart>',
// Например: ios/Classes/NativeApi.g.swift
swiftOut: '<путь к сгенерированному NativeApi.g.swift>',
// Например: android/src/main/java/ru/example/app/NativeApi.java
javaOut: '<путь к сгенерированному NativeApi.java>',
javaOptions: JavaOptions(
// Например: 'ru.example.app'
package: '<id package>',
),
// Например: ru.example.app
dartPackageName: '<Имя пакета, в котором будут использоваться файлы pigeon>',
),
)
@HostApi()
abstract class NativeHostApi {}
Next, we will use the command to generate a type-safe interface for native platforms:
flutter pub run pigeon --input lib/native_api.dart
After completing these steps, the generated files will appear in our project (plugin) in the folder lib
and also in folders android
And ios
.
Now let's connect our generated files to the platform channel. To do this, in the file method_channel.dart
let's add:
final NativeHostApi _native = NativeHostApi();
And all we have left to do on the Dart side is simply call methods from the variable _native
which will be generated according to the interface in the class NativeHostApi
.
But this is far from the end – now we need to define our pigeon-channel
in each of the native platforms.
It is worth clarifying in advance that Plugin
in the current modification – this is the class of our plugin, which implements the interface FlutterPlugin
.
Android
For the Android version, let's move on to the plugin class that implements the interface FlutterPlugin
. It is usually located along the way. android/src/main/<ID Пакета>/<Имя плагина>.java
. In this class we need to implement our generated interface for the native platform. Example:
public class Plugin implements FlutterPlugin, NativeApi.NativeHostApi {}
In the method onAttachedToEngine
initialize our pigeon-channel with setUp
:
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "plugin");
NativeApi.NativeHostApi.setUp(flutterPluginBinding.getBinaryMessenger(), this);
}
iOS
We look for the register method and modify it:
public static func register(with registrar: FlutterPluginRegistrar) {
let messenger : FlutterBinaryMessenger = registrar.messenger()
let api : NativeHostApi & NSObjectProtocol = Plugin.init()
NativeHostApiSetup.setUp(binaryMessenger: messenger, api: api)
}
Just like with Android, we implement our new Pigeon interface:
public class Plugin: NSObject, FlutterPlugin, NativeHostApi {}
In the process of adding new methods in the Dart interface, after subsequent code generation, the corresponding platform (iOS or Android) will ask to add stubs for new or changed interfaces. This will significantly simplify further development of the plugin.
Part 3. Integration of EBS-Adapter
EBS-Adapter is .aar
file on Android and .framework
for iOS, which are called ЕБС.Sdk.Adapter
. The library itself only acts as a bridge between our application and the State Services Biometrics application. The library provides a method for starting a session, authorization, and checking for the possibility of authorization.
Description of the library for the Android platform
To work with the library and the biometric system in Android, ЕБС.Sdk.Adapter
provides class EbsApi
:
Returned data | Method | Description |
---|---|---|
boolean | EbsApi.isInstalledApp(Context) | The method returns whether the application required for the adapter to work is installed. |
String | EbsApi.getAppName(Context) | The method returns the name of the application that needs to be installed for the adapter. |
String | EbsApi.getRequestInstallAppText(Context) | The method returns the text of the request to install the application required for the adapter to work. |
void | EbsApi.requestInstallApp(Context) | The method opens Google Play/AppStore in the application or browser for the user to install it. |
boolean | EbsApi.requestVerification(AppCompatActivity, VerificationRequest) | The main method calls a request for verification through the adapter in Gosuslugi Biometrics in the context of android.app.Activity |
VerificationResult | EbsApi.getVerificationResult(Intent) | The method receives the verification result via the adapter in Gosuslugi Biometrics |
Class VerificationRequest
contains the following properties:
String infoSystem – consumer system identifier;
String adapterUri – base URL for accessing the Adapter API;
String sid – session identifier;
String dboKoUri – URL of the API for receiving the verification result of the DBO KO (Information system for remote banking services for counterparty clients);
String dboKoPublicUri – public URL of DBO KO, to which the Adapter should redirect the user in case of successful completion of the process;
The main method is used for authorization EbsApi.requestVerification
. Permission is required for its implementation. ru.rtlabs.mobile.ebs.permission.VERIFICATION
which must be defined in the manifest. The method itself can work in two modes:
Automatic (
VerificationRequestMode.AUTOMATIC
) – The adapter automatically checks for the installed application “Gosuslugi Biometrics”. If it is installed, the verification process is launched. If it is not installed, a built-in dialog box is displayed with a request to install “Gosuslugi Biometrics”;Manual (
VerificationRequestMode.CUSTOM
) – The adapter does not check for the presence of the installed application “Gosuslugi Biometrics”. If the application is missing, it will returnfalse
. In this mode, you need to check the installation of “Gosuslugi Biometrics” yourself (usingEbsApi.isInstalledApp(Context)
).
By default, the adapter uses the mode VerificationRequestMode.AUTOMATIC
.
Let's consider each of the interaction methods:
VerificationRequestMode.AUTOMATIC
In the diagram, our mobile application generates an object of the class VerificationRequest
for a verification request. Next, we pass this object to the method EbsApi.requestVerification
. Permission must be granted to execute the method. ru.rtlabs.mobile.ebs.permission.VERIFICATION
.
Then with the help of Intent
we go to the mobile application “Gosuslugi Biometrics”. In the application we go through the entire necessary authorization process and receive the verification result in the method onActivityResult
(EbsApi.getVerificationResult
).
VerificationRequestMode.CUSTOM
In manual mode, everything is a little more complicated. First, we check ourselves whether the mobile application “Gosuslugi Biometrics” is installed (this can be done by calling the method EbsApi.isInstalledApp
). If the application is not installed, we cannot continue verification, but we can ask the user to install this application using the method EbsApi.requestInstallApp
. Next, as in the first mode of operation, we form an object of the class VerificationResult
and we perform a verification request using the method EbsApi.requestVerification
. Before calling, as before, we must check whether permission has been granted. ru.rtlabs.mobile.ebs.permission.VERIFICATION
.
We need to process the result in both modes by checking the state of the verification result using the property verificationResult.state
Let's look at each property:
SUCCESS – the verification process was successful and our mobile application can get the secret key in the property
verificationResult.resSecret
;CANCEL – the verification process was stopped by the user or the system;
REPEAT – the process was unsuccessful and the user requested a repeat verification;
FAILURE – the verification process ended with an error.
Description of the library for the iOS platform
On the iOS platform, the library works a little differently. For authorization in the Unified Biometric System, the library provides the class EbsSDKClient
. Let's look at what methods are available to us:
Method | Description |
---|---|
set (scheme: String, title: String, infoSystem: String) | The method sends and initializes the verification process through the State Services Biometrics MP. |
requestEbsVerification(sessionDetails: EbsSessionDetails, completion: @ escaping RequestEBSVerificationCompletion) | The method initializes the verification process via the MP “Gosuslugi Biometrics”. The result of the method comes to the completion block. If successful, the parameter does not contain an error and contains resSecret. |
openEbsInAppStore() | The method allows you to open the MP “Gosuslugi Biometrics” page in the AppStore. |
ebsAppIsInstalled() | The method returns the property of whether the State Services Biometrics MP is installed on the device. |
process(openUrl: URL, options: [UIApplication.OpenURLOptionsKey: Any]) | The method handles the opening of our application from the MP “Gosuslugi Biometrics” (called from AppDelegate) |
If the State Services Biometrics application is installed, then after calling the method requestEBSVerification
the user is authorized in the Unified Identification and Authentication System. MP “Gosuslugi Biometrics” performs the procedure for obtaining biometric samples, which it transfers to the Unified Biometric System for biometric verification. As a result of verification, our application receives a model EbsVerificationDetails
with the field resSecret
which we then process on our backend.
During the verification process, different states may return, which will allow you to control the occurrence of errors:
.success – remote verification was successful (you can get
verificationResult
);.cancel – remote verification was cancelled;
.failure – remote verification completed with an error (an error object can be obtained); The error is detailed by the following states:
.unknown – unknown error;
.ebsNotInstalled – MP “Gosuslugi Biometrics” is not installed;
.sdkIsNotConfigured – The SDK was not previously configured (method not called)
.set(…)
);.verificationFailed – remote verification was not successful.
Implementing a binary distribution
In the folder android
let's create a directory libs
and add it there .aar
file with adapter. In the file android/build.gradle
let's add a new dependency
implementation(name: '<Название файла>', ext: 'aar')
In this same file we will specify our directory lib
as a makeshift Maven repository using flatDir
. This will allow us to not add .aar
files into our main project and store them in the plugin project.
rootProject.allprojects {
repositories {
google()
mavenCentral()
flatDir {
dirs project(':<Название плагина>').file('libs')
}
}
}
To the folder iOS
let's add .xcframework
. Next, we need to determine the path to the pre-compiled framework (our EBS adapter). To do this, we will use the file .podspec
which is located in the folder ios
. All we need to add are three configuration points:
/// Указываем, что все папки внутри фреймворка сохраняются
/// при установке пода
s.preserve_paths="Frameworks/EbsSDKAdapter.xcframework/**/*"
/// Указываем компилятору использовать адаптер как дополнительный фреймворк
s.xcconfig = { 'OTHER_FLAGS' => '-xframework EbsSDKAdapter' }
/// Указываем ЕБС-адаптер фреймворк для включения в проект при установке пода
s.vendored_frameworks="Frameworks/EbsSDKAdapter.xcframework"
Interface methods for interacting with plugin methods
In the last step we wrote the interface NativeHostApi
. Now we will describe its methods in accordance with those provided by the EBS-Adapter:
abstract class NativeHostApi {
@async
bool isInstalledApp();
@async
String getAppName();
@async
String getRequestInstallAppText();
@async
bool requestInstallApp();
@async
EbsResultData requestVerification({
required String infoSystem,
required String adapterUri,
required String sid,
required String dboKoUri,
required String dbkKoPublicUri,
});
}
In the same file we will create a class EbsResultData
which will be returned if authentication is successful. Example:
class EbsResultData {
EbsResultData(
this.isError,
this.secret,
this.errorString,
);
final String? secret;
final String? errorString;
final bool isError;
}
It is very important to create the class in the same file, since the generator will parse these classes from only one file. Therefore, our class will be in one file along with the configuration and interface.
We integrate the interface into the native platform
After generating the interface in our native classes, you will be asked to add stubs to work with the plugin.
Android
@Override
public void isInstalledApp(@NonNull NativeApi.Result<Boolean> result) {}
@Override
public void getAppName(@NonNull NativeApi.Result<String> result) {}
@Override
public void getRequestInstallAppText(@NonNull NativeApi.Result<String> result) {}
@Override
public void requestInstallApp(@NonNull NativeApi.Result<Boolean> result) {}
@Override
public void requestVerification(
@NonNull String infoSystem,
@NonNull String adapterUri,
@NonNull String sid,
@NonNull String dboKoUri,
@NonNull String dbkKoPublicUri,
@NonNull NativeApi.Result<NativeApi.EbsResultData> result
) {}
iOS
func getAppName(
completion: @escaping (Result<String, Error>
) -> Void) {}
func getRequestInstallAppText(
completion: @escaping (Result<String, Error>) -> Void
) {}
func requestInstallApp(
completion: @escaping (Result<Bool, Error>) -> Void
) {}
func requestVerification(
infoSystem: String,
adapterUri: String,
sid: String,
dboKoUri: String,
dbkKoPublicUri: String,
completion: @escaping (Result<EbsResultData, Error>) -> Void
) {}
At this step, our integration is almost complete, all that remains is to describe the operation of methods in native platforms and return the result as a callback to the argument method result/completion
. For example:
@Override
public void isInstalledApp(@NonNull NativeApi.Result<Boolean> result) {
try {
boolean isAppInstalled = EbsApi.isInstalledApp(context);
result.success(isAppInstalled);
} catch (Exception e) {
result.error(
new NativeApi.FlutterError(
e.getMessage(),
e.getLocalizedMessage(),
e.getCause()
)
);
}
}
However, the question arises: how to make a verification request? On the Android platform, we will use Intent
and a new class Activity
expanded with AppCompactActivity
and the data between ours FlutterActivity
and subsidiaries EbsActivity
we will transmit using Extras
.
For iOS platform we need to register URL-Scheme
to be able to switch to the application from the State Services Biometrics application and add a key LSApplicationQueriesSchemes
with meaning ebsgu
.
Let's get back to the Android platform. In our new Activity
Let's specify the following properties:
// Используется в onRequestPermissionsResult
int REQUEST_CODE__PERMISSION = 121;
// Используется в onActivityResult
int REQUEST_CODE__VERIFICATION = 122;
// Используется для возврата результата в FlutterActivity
static int RESULT_CODE_OK = 234;
// Используется для возврата результата в FlutterActivity
static int RESULT_CODE_ERROR = 235;
// Описание ошибки
static final String cause_field = "cause";
// Секретный токен
static final String secret_field = "secret";
// Служебные Extras
static final String input_info_system = "infoSystem";
static final String input_adapter_uri = "adapterUri";
static final String input_sid = "sid";
static final String input_dbo_ko_uri = "dboKoUri";
static final String input_dbo_ko_public_uri = "dbkKoPublicUri";
We will start the verification in the overridden method onCreate
but before starting, you need to check the verification permissions. To do this, we will use the method checkSelfPermission
where we pass the permission name. If the permissions were previously accepted, we start verification. If the permissions are not accepted, we request them. When requesting permissions, it is necessary to catch the result of the request in the overridden method onRequestPermissionsResult
. Example:
@Override
public void onCreate(Bundle s) {
int hasPerm = this.checkSelfPermission(EbsApi.PERMISSION__VERIFICATION);
if (hasPerm == PackageManager.PERMISSION_GRANTED) {
/// Начинаем старт верификации
} else {
this.requestPermissions(new String[]{EbsApi.PERMISSION__VERIFICATION}, REQUEST_CODE__PERMISSION);
}
super.onCreate(s);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == REQUEST_CODE__PERMISSION) {
if(permissions.length != 0 && Objects.equals(permissions[0], EbsApi.PERMISSION__VERIFICATION)) {
if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
/// Начинаем старт верификации
} else {
/// Ошибка
}
} else {
/// Ошибка
}
}
}
Now let's figure out how to start the verification process itself. Let's create a new method processVerification()
in which we will initiate the start of verification. To start, we need to get Intent
and also create an instance of the class VerificationRequest
to which we will transfer our Extras
. Example:
private void processVerification() {
Intent intent = getIntent();
VerificationRequest request = new VerificationRequest
.Builder()
.infoSystem(intent.getStringExtra(input_info_system))
.adapterUri(intent.getStringExtra(input_adapter_uri))
.sid(intent.getStringExtra(input_sid))
.dboKoUri(intent.getStringExtra(input_dbo_ko_uri))
.dboKoPublicUri(intent.getStringExtra(input_dbo_ko_public_uri))
.build();
VerificationRequestMode verificationRequestMode = VerificationRequestMode.AUTOMATIC;
EbsApi.requestVerification(this, request, REQUEST_CODE__VERIFICATION, verificationRequestMode);
}
We add this method to onCreate
. The result is returned to onActivityResult
there we will catch our final result using the method EbsApi.getVerificationResult
.
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if(requestCode == REQUEST_CODE__VERIFICATION && resultCode == Activity.RESULT_OK) {
VerificationResult result = EbsApi.getVerificationResult(data);
switch (result.getState()) {
case SUCCESS: {
/// Возвращаем результат
break;
}
case CANCEL: {
/// Операция отменена
break;
}
case FAILURE: {
/// Произошла ошибка
break;
}
case REPEAT: {
/// Произошла ошибка, пожалуйста повторите
break;
}
}
}
super.onActivityResult(requestCode, resultCode, data);
}
Let's return the result to our FlutterActivity
and from there directly the result into our Flutter application. To do this, in the new EbsActivity we will add two simple methods – processError
And processResult
. The first one will return an error, the second one – the result:
private void processResult(@NonNull VerificationResult result) {
if(result.isValid()) {
Intent data = new Intent();
data.putExtra(secret_field, result.getSecret());
setResult(RESULT_CODE_OK, data);
finish();
} else {
processError("Ошибка валидации");
}
}
private void processError(@NonNull String cause) {
Intent data = new Intent();
data.putExtra(cause_field, cause);
setResult(RESULT_CODE_ERROR, data);
finish();
}
In the first method we take the result and check if it is valid (state SUCCESS
and a non-empty secret token). In the second method, we accept the error description and return it. We add these methods to onActivityResult
.
Now let's call EbsActivity
via a plugin. To do this, in the overridden method requestVerification
let's add Intent
which will cause EbsActivity
. IN Intent
we will also add Extras
. Example:
@Override
public void requestVerification(
@NonNull String infoSystem,
@NonNull String adapterUri,
@NonNull String sid,
@NonNull String dboKoUri,
@NonNull String dbkKoPublicUri,
@NonNull NativeApi.Result<NativeApi.EbsResultData> res
) {
Intent intent = new Intent(binding, EbsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(EbsActivity.input_info_system, infoSystem);
intent.putExtra(EbsActivity.input_adapter_uri, adapterUri);
intent.putExtra(EbsActivity.input_sid, sid);
intent.putExtra(EbsActivity.input_dbo_ko_uri, dboKoUri);
intent.putExtra(EbsActivity.input_dbo_ko_public_uri, dbkKoPublicUri);
binding.startActivityForResult(intent, REQUEST_CODE);
result = res;
}
In the method onActivityResult
in the plugin we will catch the result of the work EbsActivity
checking requestCode
:
@Override
public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_CODE) {
NativeApi.EbsResultData resData = new NativeApi.EbsResultData
.Builder()
.setIsError(resultCode == EbsActivity.RESULT_CODE_ERROR)
.setSecret(data != null && resultCode == EbsActivity.RESULT_CODE_OK ? data.getStringExtra(EbsActivity.secret_field) : null)
.setErrorString(data != null && resultCode == EbsActivity.RESULT_CODE_ERROR ? data.getStringExtra(EbsActivity.secret_field) : null)
.build();
if(result != null) {
result.success(resData);
result = null;
}
}
return false;
}
In this case we are comparing requestCode
. If it is equal to the code we defined in requestVerification
then this is our case, and we can get from Intent
all the necessary information and return this information using a call result.success()
.
For the iOS platform, everything is a little simpler. In the file AppDelegate.swift
we implement the method:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
EbsSDKClient.shared.process(openUrl: url, options: options)
return true
}
In our plugin method requestVerification
First, let's call the method set(scheme: String, title: String, infoSystem: String)
where we will pass the parameters from the plugin method, as well as the parameter scheme
in which you need to add the registered URL-Scheme. Then simply call requestEBSVerification
and we get a model EbsVerificationDetails
which we process in accordance with the states it contains. We return the result to the application being developed using the method completion(.success(<EbsResultData>))
which generates Pigeon.
Conclusion
So, we have looked at how you can integrate the EBS system into a mobile application on Flutter, using Pigeon to integrate the framework and create a full-fledged plugin. I hope this material will be useful to you. Have a nice day and easy integrations!