DEV Community

Cover image for Signing message using Metamask in Flutter
Bhaskar
Bhaskar

Posted on

Signing message using Metamask in Flutter

When it comes to decentralized applications, signed messages provide a way to authenticate a user or specific data. It plays a crucial role when it comes to meta-transactions (off-chain transactions) and helps in preventing Replay Attacks.

What we are going to build?

This blog is a continuation of our Building with Flutter and Metamask series where we learn how to build an app that can directly interact with the blockchain using Metamask. In this blog, we will learn how we can use Metamask to sign a particular message. After this tutorial, there will have an additional “Sign Message” step in the login process.

Signing Message GIF

The user signing the message represents the user agrees to the Terms and Conditions provided by our app. While building this feature, we will also learn about how these signatures are verified by the t contracts.

Why sign a message?

A signature is the most common form to denote authorization. For example, in the real world, signing any legal document implies that the signer agrees to all the terms and conditions mentioned in the signed document. In the blockchain ecosystem, providing a signature implies a similar meaning. The user is asked to sign a message, and this signature can be later verified by the smart contract for performing several actions. The Magic of Digital Signature on Ethereum provides an in-depth view of digital signature and signature verification.

EIP-191 and EIP-712 are two of the most popular proposed standardization for signing messages in the Ethereum blockchain ecosystem. EIP-712 provides a more human-readable format of data while signing it, whereas EIP-191 is much more simple in implementing but users are asked to sign a hash, that is difficult to comprehend. Although EIP-712 provides a far superior user experience, it is still in its early stages of development.

For this tutorial, we will keep it very simple and provide our users with a hash of a simple text message for signing. We can also provide the raw text for signing, but this would make the process of signature verification more difficult as we must pass the length of the message signed for verification. We use keccak256 for hashing the message before signing it. Using keccak256 for hashing the message ensures that the hash will always be 256-bits long, regardless of the actual length of the message. It makes the process of signature verification significantly easier as the length of the signed message will always be 256-bit long. This will be more clear as we proceed with our application development and testing.

Pre-requisites

  • Readers are highly encouraged to complete the first part of this series.
  • Have all the pre-requisites mentioned in the first part.
  • You can continue with your code, however, in this tutorial, we will start from login-with-metamask branch of the FlutterAppWithMetamask repo.
  • Remix-ide for writing and testing smart contracts.

How to use signatures

Before we proceed with our application development, it is important to understand how messages are signed and verified using a smart contract. Let’s assume we want to sign the message “Hello Developers!”, the steps followed for this are shown in the following diagram:

Flow Diagram for signing message

The steps followed here are as follows:

  1. We start with the Raw data, in this example, it is the string “Hello Developers!”. In more advanced situations, the raw data can be something like serialized JSON.
  2. We use the keccak256 hashing algorithm to generate the hash of the Raw message. It is important to note that the generated hash is in form of a hexadecimal number.
  3. Before signing, the passed message is structured in the following format:

    "\x19Ethereum Signed Message:\n" + length(message) + message
    

    The prefix denotes that the signed message is intended to be used in the Ethereum blockchain network. Since we are already hashing the raw message before passing it for signing, we know that the size of the message will be 32 bytes.

    Hence if we follow the above format of hashing the raw message before sending it to get signed, we can deterministically say that the prefix will always be “\xEthereum Signed Message:\n32”.

  4. Finally, the formatted message is signed using the private key and a signature is produced in hexadecimal number format.

Using solidity, we can write a function that will take in the generated signature and the raw message to compute the account that signed the message. This forms a basis to verify whether the concerned account has provided the required signatures.

Signature verification using solidity

Although writing a smart contract for signature verification is a complex topic and demands a tutorial of its own, the following YouTube video proves to be very helpful:

https://www.youtube.com/watch?v=vYwYe-Gv_XI&t=647s

I will use a smart contract similar to the one shown in the tutorial for demonstrating the above-explained idea and testing the generated signature in a later phase of this blog. The smart contract I am using is:

//SPDX-License-Identifier: Unlicensed

pragma solidity ^0.8.4;

contract SignaturesInEth {

    function _split(bytes memory signature) internal pure returns(bytes32 r, bytes32 s, uint8 v) {
        require(signature.length == 65, "Signature must be 65 bytes long");
        assembly {
            r := mload(add(signature, 32))
            s := mload(add(signature, 64))
            v := byte(0, mload(add(signature, 96)))
        }
    }

    function getMessageHash(string memory message) public pure returns(bytes32) {
        return keccak256(abi.encodePacked(message));
    }

    function getEthSignedMessageHash(bytes32 messageHash) public pure returns(bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
    }

    function recover(bytes32 ethSignedMessageHash, bytes memory signature) public pure returns(address) {
        (bytes32 r, bytes32 s, uint8 v) = _split(signature);
        return ecrecover(ethSignedMessageHash, v, r, s);
    }

    function verifySignature(string memory message, bytes memory signature) public pure returns(address) {
        bytes32 messageHash = getMessageHash(message);
        bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);

        return recover(ethSignedMessageHash, signature);
    }

}
Enter fullscreen mode Exit fullscreen mode

Sample verification

Let’s take a small tangent and verify the details shown in the above flow diagram. The details shown in the flow diagram are as follows:

Raw Message: ****Hello Developers!
Hash of Raw Message: 0x8a67fef1b0a61b7d4e8f0921c7c93372ab0e38f1317b2e9431cdca324c041792
Eth-Signed Message: 0x21d8c6f7bbaa104e61dfba68f68eabdfe47968b3f9b3fea6d9146214743cb01d
Signature: 0xa01b7764293ccab794ef74b7d3d21f88f62eeac7acc2959c422d254a3aadb60f08f7c4afd25b088af870772d20df8482d85f0b675441bbe61474d75c5c2663761b
Signer: ****0xcDb333BCE61C1E4a3Da9F4a2831e23b155f3949B
Enter fullscreen mode Exit fullscreen mode

We can use the above smart contract to verify this details. Paste the contract into Remix and deploy it to the Javascript VM.

To start with, we can use the verify function passing in the Raw Message and Signature to obtain the signing account. We can then verify manually that the returned signer is the correct account signing the message.

Verification Example

Similarly, we can also use the other functions to calculate the other values specified in the flow diagram. We will again use this contract after we have completed our app but for now, let's back to our main objective of app development.

Developing the App

If you want to continue with your code, that is highly encouraged, however, if you want to have a common starting point with this tutorial, clone this repo and create a new branch, called signing-message from the login-with-metamask branch.

git clone https://github.com/BhaskarDutta2209/FlutterAppWithMetamask
cd FlutterAppWithMetamask
git checkout login-with-metamask
git checkout -b signing-message
Enter fullscreen mode Exit fullscreen mode

Open the project folder in your code editor of choice and we are Ready To Go!

Installing dependencies

For this tutorial, we will be using keccak256 for hashing our message. For doing so, we will need the web3dart package. For this blog we will be limiting ourselves to only the crypto library, however, in future blogs, this same package will also be used for interacting with the smart contracts. To add the package, type the following command:

flutter pub add web3dart
Enter fullscreen mode Exit fullscreen mode

Creating a function for signing message

Open the login_page.dart file inside the _LoginPageState class where we have previously defined _session and _uri variables, define a new variable called _signature for storing the signed data.

var _session, _uri, _signature;
Enter fullscreen mode Exit fullscreen mode

Now inside the same class we define a new function called signMessageWithMetamask as shown below:

signMessageWithMetamask(BuildContext context, String message) async {}
Enter fullscreen mode Exit fullscreen mode

Besides the BuildContext, this function takes in a String parameter that represents the message to be signed. In the later section of this blog, we will see that the message is nothing but the keccak256 hash of the data we want to be signed.

Writing the function body

The main content of the function body is as follows:

signMessageWithMetamask(BuildContext context, String message) async {
  if (connector.connected) {
    try {
      print("Message received");
      print(message);

      EthereumWalletConnectProvider provider =
          EthereumWalletConnectProvider(connector);
      launchUrlString(_uri, mode: LaunchMode.externalApplication);
      var signature = await provider.personalSign(
          message: message, address: _session.accounts[0], password: "");
      print(signature);
      setState(() {
        _signature = signature;
      });
    } catch (exp) {
      print("Error while signing transaction");
      print(exp);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we are doing the following:

  • First, we check whether we are already connected with an external wallet using the connect variable defined in the previous tutorial.
  • If we are connected, we proceed with signing our message. We are using try-catch block for error handling and are printing the error to the console. In a production-ready app, we should properly notify the user about any caused error. The most common cause where an error can be generated is if the user rejects signing the message, after the metamask popup.
  • Before signing the message, we must define a provider. This step may seem familiar if you have used web3.js or ethers.js before. Since our targeted blockchain is Polygon, which is a fork of the Ethereum blockchain network, the provider type is EthereumWalletConnectProvider and we pass the connector as an argument.
  • We want the Metamask app to pop up for signing the message. To open the Metamask app, we use the launchUrlString function passing in the _uri. This is similar to how we opened Metamask during login.

    Note: Here we are not using await keyword, because we want to open Metamask and continue with the rest of our function execution and not wait for Metamask to close. After a user signs the message, metamask will close automatically.

  • Now we use the personalSign method in the provider object defined for signing the message passed as a function parameter. This method also takes the signing account as a parameter. We use _seesion.accounts[0] to pass the current metamask account connected with the app.

    Here we are using await keyword because we want the function execution to wait until the user signs the data.

  • Finally, we use setState method to update the value of _signature variable with the latest obtained signature. Using the setState function will also re-render the app screen and will display the updated information.

Adding the button

We want to add a simple Sign Message button before the Slide to login slider. For showing the button we will use a similar ternary-operator syntax used in our previous tutorial. We check whether the _signature variable is null as a null value would signify that the message is not signed. If the value of _signature is set, aka the message is signed by the user, we display the signature on the app screen.

Inside our login_page.dart file, we replace the SliderButton() section with the following code:

...
(_signature == null)
  ? Container(
      alignment: Alignment.center,
      child: ElevatedButton(
        onPressed: () =>
          signMessageWithMetamask(
            context,
            generateSessionMessage(
              _session.accounts[0])),
        child: const Text('Sign Message')),
    )
  : Column(
      crossAxisAlignment:
        CrossAxisAlignment.center,
      children: [
        Row(
          children: [
            Text(
              "Signature: ",
              style: GoogleFonts.merriweather(
                fontWeight: FontWeight.bold,
                fontSize: 16),
            ),
            Text(
              truncateString(
                _signature.toString(), 4, 2),
              style: GoogleFonts.inconsolata(
                fontSize: 16))
          ],
        ),
        const SizedBox(height: 20),
        SliderButton(
          action: () async {
            // TODO: Navigate to main page
          },
          label: const Text('Slide to login'),
          icon: const Icon(Icons.check),
        )
      ],
    )
...
Enter fullscreen mode Exit fullscreen mode

Sorry for the mess that this Dart code is, but if you look closely into the code, it only does the following things:

  • If the value of _signature is null, we create an ElevatedButton with the text Sign Message. On clicking this button, the signMessageWithMetamask function is called passing in the current context and the message to be signed as a parameter. To obtain the message to be signed we are using the generateSessionMessage function that takes in the currently connected account as a parameter. We will define this function in the next section.
  • If the value of _signature is not null, we show the signature on the app screen. Since the signature is a very long string, we use a function called truncateString to truncate it in the AAAA…BB format. We will define this function also in the next section.

Creating the helper functions

Inside the utils folder, we create a new file named helperfunctions.dart. In this file, we will specify the two helper functions used above. We start with the truncateString function, which is the easier of the two to implement.

String truncateString(String text, int front, int end) {
  int size = front + end;

  if (text.length > size) {
    String finalString =
        "${text.substring(0, front)}...${text.substring(text.length - end)}";
    return finalString;
  }

  return text;
}
Enter fullscreen mode Exit fullscreen mode

In this function:

  • front and end represent the number of characters to be displayed before and after the truncation symbol () respectively.
  • We check if it’s possible to truncate the string based on the passed parameters, i.e. the passed text has a length greater than front + end. If so, we use the substring method to truncate the string to our requirement.
  • Finally, we return the truncated string or the original string if truncating is not possible.

Now we define the generateSessionMessage function. Since in this function we will need to implement keccak256 hashing, we import the web3dart/crypto.dart package by adding the following line at the top of our script:

import 'package:web3dart/crypto.dart';
Enter fullscreen mode Exit fullscreen mode

This will give us access to a function called keccak256Utf8 which uses the keccak256 hashing algorithm. This will return the data in form of an array of bytes. We will then use another function called bytesToHex to convert this array to hexadecimal string format. We prefix “0x” with the generated string to denote that the string is in hexadecimal format. For simplicity, we have initialized the raw message as a string containing the account address of the user. However, it is possible and encouraged to use a more complex data structure for the raw message e.g. serialized JSON. The complete function looks like this:

String generateSessionMessage(String accountAddress) {
  String message = 'Hello $accountAddress, welcome to our app. By signing this message you agree to learn and have fun with blockchain';
    print(message);

  var hash = keccakUtf8(message);
  final hashString = '0x${bytesToHex(hash).toString()}';

  return hashString;
}
Enter fullscreen mode Exit fullscreen mode

Finally, we import our helper functions in our login_page.dart file using the following import statement:

import 'package:my_app/utils/helperfunctions.dart';
Enter fullscreen mode Exit fullscreen mode

Running our app

Finally, we are ready to run our app 🤞🏾🤞🏾🤞🏾!

If everything was done correctly, on running the app, we will be presented with a flow similar to our previous tutorial. However, when we are connected with the correct network, we will be presented with an additional “Sign Message” button. On clicking on it, we are treated with a Metamask popup that looks something like the following:

Sign message popup

Here you can notice one of the challenges of using the Metamask app, that is the message shown is not human readable. Due to some internal bug in the app, the message section is not able to show the hexadecimal hash properly.

Nevertheless, when we click on the Sign button, we are returned to our app screen. Now we can see the truncated version of the signature along with our old Slide to login slider.

Details in App Screen

You can get the complete signature from the console. In my case the details are as follows:

Account: 0x11Ea3D9B50116986C77E17Fa177D56fae0b73631
Signature: 0xd4e8b7997d5e244981965a5f301658f780242ff51226e0fc6af21564867a0d560292b1aa17e41ca836111b7413bbbd3a0a430c0715ae511dd206396c8bed7abe1b
Enter fullscreen mode Exit fullscreen mode

Verifying the signature using a smart contract

Now we get back to our smart contract and try to verify this signature. Depending on the account you used, the message and the signature will be different. In my case:

Message: "Hello 0x11ea3d9b50116986c77e17fa177d56fae0b73631, welcome to our app. By signing this message you agree to learn and have fun with blockchain"
Enter fullscreen mode Exit fullscreen mode

We call the verifySignature function with the above parameters to obtain the signing account.

Verify

We can cross-check the returned address with the address used for signing. This proves that by using the signature we can determine the account that provided the signature. Mission Accomplished 😎🎉!

Wrapping up

In this tutorial, we learned about signing messages using Ethereum wallets like Metamask. We also learned how we can use Metamask in a Flutter app for generating the signature of a String message. Finally, we verified our signature using a simple smart contract.

The code for this project is uploaded to Github here. The updated code for this tutorial will be present in the signing-message branch.

About me

I am working in the domain of blockchain technology for more than 2years now. I aspire to learn new technologies targeted at solving real-world problems and write tutorials with unique approaches. Kindly follow me to stay updated as I write new tutorials and show you support through reactions and comments.

I am also looking forward to writing sponsored articles and creating YouTube videos based on my articles. If you want to connect with me or provide your feedback, you can find me on LinkedIn, Twitter, or through my mail.

We will meet again with another new tutorial or blog, till then stay safe, spend time with your family and KEEP BUIDLING!

Discussion (0)