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:

  1. Checking whether the State Services Biometrics application is installed on the user’s device.

  2. 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:

  1. Create a plugin and implement it Pigeon;

  2. Embed the library into the plugin EBS Adapter;

  3. 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 liband 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 _nativewhich 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.VERIFICATIONwhich 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 return false. In this mode, you need to check the installation of “Gosuslugi Biometrics” yourself (using EbsApi.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 resSecretwhich 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 .podspecwhich 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 EbsResultDatawhich 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 Activityexpanded with AppCompactActivityand 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 onCreatebut before starting, you need to check the verification permissions. To do this, we will use the method checkSelfPermissionwhere 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 Intentand also create an instance of the class VerificationRequestto 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 Intentwhich 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 requestVerificationthen 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 schemein which you need to add the registered URL-Scheme. Then simply call requestEBSVerification and we get a model EbsVerificationDetailswhich 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!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *