Présentation

La notarisation d’un fichier est le fait de garantir le contenu et l’origine d’un fichier au moyen d’un tiers de confiance.

Nous allons ici mettre en place un moyen de garantir, grâce à la blockchain Ethereum, l’intégrité d’un fichier déposé dans Google Cloud Storage.

L’opération consiste à stocker sur la blockchain un hash du contenu du fichier. Pour vérifier l’intégrité, il suffira de comparer le hash d’un fichier cible à celui enregistré. Si les hash correspondent, c’est que le fichier cible est le même que celui qui a été notarisé.

La blockchain agit comme tiers de confiance en garantissant que le hash stocké n’a pas été altéré depuis sa création. Elle fournit également le timestamp de la notarisation afin de faire valoir une éventuelle antériorité.

Vue d’ensemble

Notarisation avec Ethereum et GCP, vue d’ensemble

Composants

Nous allons utiliser différents composants :

  • Un bucket Cloud Storage pour le dépôt de fichier
  • Une Cloud Function, en Node.js dans notre exemple, pour déclencher la notarisation
  • GCP Secret Manager, pour stocker de façon sécurisée les clés privées et clés d’API
  • Ethereum, notamment le testnet Ropsten, via le provider Infura, pour effectuer la notarisation
  • Le framework javascript Web3.js pour communiquer avec Ethereum
  • Le framework javascript Ethereumjs-tx pour gérer la création de transactions signées
  • Un smart contract écrit en Solidity et déployé sur Ethereum via Remix pour gérer les notarisations
  • Une application web permettant de vérifier l’intégrité des fichiers notarisés
  • Metamask, extension de navigateur qui permettra à l’application web d’accéder à la blockchain et d’identifier l’utilisateur

L’objectif de cet exemple étant de montrer un cas d’usage, nous partons du principe que vous êtes déjà familier avec Ethereum, le déploiement de contrat, la notion d’ABI

Procédure pas à pas

#1 – Créer le bucket dans Cloud Storage

Dans la console GCP, Stockage > Navigateur > Créer un bucket

Créons un bucket que nous appelons ethereum-notary-bucket. (Le nom d’un bucket étant unique globalement, vous ne pourrez pas donner le même nom au votre). Il se peut que vous ayez des permissions d’accès à paramétrer, selon votre configuration GCP.

Création du bucket
Création du bucket

#2 – Créer et déployer le smart contract

Les éléments à notariser seront stockés dans un smart contract déployé sur la blockchain.

Ce contrat contient 3 méthodes :

  • notarizeFile notarise un fichier en associant son hash à son nom, l’adresse de son créateur et le timestamp de la notarisation.
  • checkFile permettra ensuite de vérifier l’intégrité d’un fichier
  • getFilesByAddress retourne les notarisations effectuées par un utilisateur donné

Pour cela, il gère une structure NotarizedFile qui encapsule les données d’une notarisation. Les notarisations seront stockées dans un tableau. Une second structure de données de type mapping associera une liste de notarisations pour chaque adresse émettrice de transactions, afin de facilement retrouver les éléments créés par un utilisateur.

Le contrat va également émettre deux événements :

  • FileNotarized quand un fichier sera notarisé
  • FileChecked quand une vérification aura lieu (attention, la vérification peut se faire au moyen d’un call, gratuit car ne générant pas de transaction et qui n’émettra donc pas cet événement. Pour que l’événement soit émis, on devra appeler cette fonction au moyen d’une transaction. Au choix.)
pragma solidity >=0.6.0;
pragma experimental ABIEncoderV2;

contract Notary {

    event FileNotarized(uint fileId, string fileName, uint time, address indexed owner, string hash);
    event FileChecked(uint fileId, string givenFileName, uint time, address indexed checker, string givenHash);

    struct NotarizedFile {
        uint id;
        address owner;
        string fileName;
        uint time;
        string hash;
    }

    NotarizedFile[] public files;
    mapping(address => NotarizedFile[]) public notarizedFiles;

    constructor()  {

    }

    function notarizeFile(string memory _fileName, string memory _hash) public {
        uint id = files.length;
        NotarizedFile memory file = NotarizedFile(id, msg.sender, _fileName, block.timestamp, _hash);
        files.push(file);
        notarizedFiles[msg.sender].push(file);

        emit FileNotarized(file.id, file.fileName, file.time, file.owner, file.hash);
    }


    function checkFile(uint _fileId, string memory _hash, string memory _fileName) public returns(bool) {
        emit FileChecked(_fileId, _fileName, block.timestamp, msg.sender, _hash);

        string memory fileHash = files[_fileId].hash;
        string memory fileName = files[_fileId].fileName;

        return keccak256(abi.encodePacked(fileHash)) == keccak256(abi.encodePacked(_hash)) &&
                keccak256(abi.encodePacked(fileName)) == keccak256(abi.encodePacked(_fileName));
    }

    function getFilesByAddress(address  _address) public view returns(NotarizedFile[] memory) {
        return notarizedFiles[_address];
    }
}

On peut maintenant déployer ce contrat, avec Remix par exemple.

#3 – Sécuriser les secrets dans Secret Manager

Dans la console GCP > Sécurité > Gestionnaire de secrets > Créer un code secret, créer 3 secrets :

  • ethereum-notary-infura-id : l’ID du projet créé dans Infura
  • ethereum-notary-infura-secret : le secret fourni par Infura
  • ethereum-notary-private-key : la clé privée associée à l’adresse Ethereum qui sera utilisée pour effectuer les transactions

Voilà, nos secrets sont protégés. Nous pourrons y accéder depuis la Cloud Function.

#4 – Création de la Cloud Function

Connexion à la blockchain

Nous allons initier la fonction en créant la connexion à la blockchain. Pour cela, nous avons en amont créé un projet sur Infura.

Infura est une plateforme qui fournit une API d’accès à Ethereum. Nous allons utiliser un URL de connexion Infura de la même façon que pour se connecter à une blockchain locale. Il est donc tout à fait possible de réaliser cet exemple avec n’importe quel accès à Ethereum (noeud local, Ganache …).

Nous allons créer 2 fonctions, getSecret qui va permettre de récupérer un secret dans Secret Manager et initBlockchain qui va récupérer les informations de connexion et utiliser Web3 pour se connecter à la blockchain et initialiser un objet permettant d’interagir avec le smart contract. (Il ne faut pas oublier de fournir l’ABI du contrat, supprimé ici pour des raisons de lisibilité.)

Dans notre exemple, nous allons toujours envoyer les transactions avec le même compte. L’adresse publique du compte et l’adresse du smart contract sont récupérées depuis les variables d’environnement.

const util = require("util");
const Web3 = require("web3");
const EthereumTx = require('ethereumjs-tx').Transaction
const {Storage} = require('@google-cloud/storage');
const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');

// The smart contract ABI
const notaryABI = [...];

// Read Ethereum account and smart contract address form environment variables
const account = process.env.ACCOUNT;
const contractAddress = process.env.CONTRACT_ADDRESS;

var notaryContract = null;
var web3;

/**
* Retrieves a secret from GCP Secret Manager
*/
async function getSecret(secretName){
	const client = new SecretManagerServiceClient();
	const [accessResponse] = await client.accessSecretVersion({
		name: "projects/<GCP project ID here>/secrets/"+secretName+"/versions/latest"
	});
	const responsePayload = accessResponse.payload.data.toString('utf8');
	return responsePayload;
}

/**
* Connect to Ethereum using Infura and create contract object
*/
async function initBlockchain() {
	console.log("Connecting to blockchain");
	console.assert(account, "No account provided");
	console.assert(contractAddress, "No contract address provided");

	// get Infura secrets to build connection URI
	var infuraId = await getSecret("ethereum-notary-infura-id");
	var infuraSecret = await getSecret("ethereum-notary-infura-secret");
	var ethereumProvider = "https://:"+infuraSecret+"@ropsten.infura.io/v3/"+infuraId;

	// Connect to Infura
	var web3Provider = new Web3.providers.HttpProvider(ethereumProvider);
	web3 = new Web3(web3Provider);
	console.log("Use account "+account);
	console.log("Load contract at "+contractAddress);
	try {
		// Create contract object from ABI and contract address to interact with smart contract
		notaryContract =  new web3.eth.Contract(notaryABI, contractAddress);
	}
	catch(error) {
		console.error("Error loading contract : "+error);
	}
}

Création de la transaction

L’utilisation de Infura nous impose une contrainte de taille : il est impossible de passer par un wallet qui va se charger de signer les transactions pour nous, comme une extension Metamask dans le navigateur pourrait le faire.

Nous allons donc devoir forger et signer une transaction brute. Le processus est plus compliqué que pour une transaction simple via un wallet, mais il a l’avantage de nous faire rentrer un peu plus dans le fonctionnement d’Ethereum.

Nous allons créer la fonction sendToBlockchain, qui prend en paramètre un nom et un hash de fichier, ainsi que l’adresse à utiliser pour la notarisation, et les envoie au smart contract.

La clé privée associée à l’adresse publique, utilisée pour envoyer la transaction, est récupérée dans Secret Manager de la même façon que dans la fonction précédente.

Vos clés privées doivent bien entendu rester secrètes puisqu’elles permettront à quiconque les possède de prendre le contrôle sur votre compte.

Dans le code ci-dessous, nous voyons la création de la transaction, et notamment la génération du payload au moyen denotaryContract.methods.notarizeFile(fileName, fileHash).encodeABI(), qui va nous permettre de récupérer facilement l’instruction d’appel à la méthode, avec ses paramètres, directement encodée dans le bon format ; agrémentée des informations de transaction (adresse, valeur à transférer, prix du gas …). Cette transaction sera alors signée manuellement avec la clé privée et envoyée en tant que transaction « brute ».

/**
* Send information to notarization smart contract to write it in the blockchain
* @param account the account public key to use to create the transaction
* @param fileName the name of file to notarize
* @param fileHash the hash of file to notarize
*/
async function sendToBlockchain(account, fileName, fileHash) {

	// Get current transaction nonce for given account, mandatory to create signed transaction
	web3.eth.getTransactionCount(account, async function (err, nonce) {
		let gasPrice = await web3.eth.getGasPrice();

		// Build transaction body
		const txParams = {
			nonce: nonce,
			gasPrice: web3.utils.toHex(gasPrice),
			gasLimit: web3.utils.toHex('800000'),
			to: contractAddress,
			value: '0x00', // no Ether transfered
			// get smart contract 'notarizeFile' call with parameters, encoded to be wrapped in the transaction
			data: notaryContract.methods.notarizeFile(fileName, fileHash).encodeABI()
		}

		// Get private key
		const privateKey = await getSecret("ethereum-notary-private-key");
		const bufferedPrivateKey = Buffer.from(
		  privateKey,
		  'hex',
		)
		
		// Build and sign raw transaction
		const tx = new EthereumTx(txParams,{ chain: 'ropsten', hardfork: 'petersburg' });
		tx.sign(bufferedPrivateKey);
		const serializedTx = tx.serialize().toString('hex');

		var raw = '0x' + serializedTx;

		// Send transaction and deal with events
		web3.eth.sendSignedTransaction(raw)
		.once('sending', function(payload){ console.log("Send transaction"); })
		.once('sent', function(payload){  })
		.once('transactionHash', function(hash){ console.log("Tx hash: "+hash); })
		.once('receipt', function(receipt){  })
		.on('confirmation', function(confNumber, receipt, latestBlockHash){  })
		.on('error', function(error){ console.error(error); })
		.then(function(receipt){
		    console.log("Mined in block "+receipt.blockNumber);
		});
	});
}

Déclenchement de la notarisation

Nous allons maintenant créer le point d’entrée de la Cloud Function.

Cette fonction prend en paramètre les objets event et context standards passés par GCP en paramètre de chaque appel à une Cloud Function. Elle doit être exportée pour être exécutable à l’extérieur du module.

La fonction va tout d’abord initialiser la connexion à la blockchain et la création d’un objet représentant le smart contract, grâce à la fonction initBlockchain. Puis elle va récupérer les informations du fichier que l’on vient de télécharger et se connecter à Cloud Storage pour récupérer le contenu du fichier dans le bucket.

Ensuite on calcule le hash sha256 à partir du contenu. On va alors pouvoir appeler la fonction sendToBlockchain définie précédemment avec tous ces éléments, afin de les intégrer dans une transaction à destination du smart contract.

/**
* Starts notarization, this function is called when a file is uploaded in the Storage bucket
* @param event the event object sent when cloud function is triggered
* @param context the context object sent when cloud function is triggered
*/
exports.notarize = async function(event, context) {
	await initBlockchain();

	console.log("Read "+event.name+" from "+event.bucket);
	// Connect to bucket
	const storage = new Storage();
	const bucket = storage.bucket(event.bucket);
	const remoteFile = bucket.file(event.name);
	var content = "";
	// Read file from bucket
	remoteFile.createReadStream()
		.on("data", function(data) { content += data.toString(); })
		.on('error', function(err) { console.log(err); })
		.on('response', function(response) { })
		.on('end', function() {
			// When file is fully read, compute sha256 hash of content
			const hash = require("crypto").createHash('sha256').update(content).digest().toString("hex");
			console.log("File hash: "+hash);
			// Write data to blockchain
			sendToBlockchain(account, event.name, hash);
		});
}

Création de la Cloud Function

Maintenant que nous avons tous ces éléments, nous pouvons les rassembler et les placer dans une Cloud Function dont le déclenchement sera un ajout de fichier dans le bucket créé précédemment.

Dans la console GCP, Cloud Function > Créer une fonction. Choisir « Cloud Storage » en tant que déclencheur. puis « Finaliser/Créer » comme event type, afin d’être appelé lors de l’ajout d’un fichier. Et enfin, dans Bucket, sélectionner le bucket créé précédemment.

Création de la Cloud Function
Création de la Cloud Function

Nous devons aussi définir les 2 variables d’environnement : ACCOUNT (adresse publique du compte à utiliser pour notariser) et CONTRACT_ADDRESS (l’adresse du smart contract déployé plus tôt). Ces deux variables sont récupérées dans le code de la fonction par process.env.ACCOUNT et process.env.CONTRACT_ADDRESS.

Variables d'environnement
Variables d’environnement

Dans la page suivante, nous allons définir le code de la fonction.

Il est composé des 3 portions de code, vues aux étapes précédentes. Il ne faut pas oublier d’indiquer la fonction notarize comme point d’entrée :

Code et point d'entrée
Code et point d’entrée

Le contenu du fichier package.json, avec les dépendances aux modules utilisés par la fonction, peut être défini de cette façon :

{
  "name": "sample-cloud-storage",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/secret-manager": "^3.2.0",
    "@google-cloud/storage": "^5.3.0",
    "ethereumjs-tx": "^2.1.2",
    "web3": "^1.3.0"
  }
}

#5 – Interface de vérification

Nous allons développer une petite application web qui permettra de vérifier les fichiers notarisés. Elle utilisera également le framework Web3 pour accéder au smart contract. Mais cette fois, elle ne passera pas par Infura pour accéder à Ethereum.

Comme elle s’exécutera dans le navigateur, nous pourrons utiliser Metamask comme wallet et point d’accès. Tout le code pourra être développé en javascript et s’exécuter côté client. Par contre, pour fonctionner, Web3 nécessite d’être intégré à une page exécutée côté serveur. Il nous faudra donc une application serveur Node.js qui servira notre page HTML.

Nous allons aussi avoir besoin de calculer le hash des fichiers à vérifier. Pour cela, nous allons créer un service accessible par Websocket qui fournira le hash d’un fichier sélectionné dans un formulaire HTML.

La méthode de calcul du hash dans la websocket doit être la même que celle utilisée par la Cloud Function. (On aurait pu aussi faire que la Cloud Function appelle la websocket pour garantir que le hachage soit identique)

Pour effectuer tout ça, 3 fichiers :

  • index.html : l’interface de l’application de vérification
  • index.js : fichier Node.js qui va servir la page index.html et qui va également exposer une websocket de calcul de hash
  • blockchain.js : fichier chargé par index.html, qui va interagir avec la blockchain et afficher les résultats dans la page web

N’oubliez pas d’inclure le fichier web3.min.js du framework Web3.

Côté serveur

Un fichier Node.js index.js qui fait 2 choses :

  • Servir la page HTML définie juste après, afin de rendre disponible Web3.
  • Créer une websocket qui prend en entrée un contenu de fichier et retourne son hash (sha256).
var express = require('express');

/* To serve web3 index.html file */

var app = express();
app.use('/', express.static(__dirname + '/'));

app.get('/', function(request, response){
	response.sendFile('index.html', {root: __dirname });
});

app.listen(3000, () => {
  console.log("Server started on port 3000");
});


/* Websocket that computes hash for a given content */

var WebSocketServer = require("ws").Server;
var ws = new WebSocketServer( { port: 8100 } );

console.log("Websocket started on port 8100");

ws.on('connection', function (ws) {
  console.log("Socket connected");

  ws.on("message", function (str) {
        var hash = require("crypto").createHash('sha256').update(str).digest().toString("hex");
        console.log("hash : "+hash);
        ws.send(hash);
    })

    ws.on("close", function() {
        console.log("Socket closed");
    })
});

Page web

Voici une page web simple, basée sur Bootstrap, qui affiche des informations de connexion à la blockchain (pour vérifier que la connexion est effectuée avec les bons paramètres) et la liste des fichiers notarisés par l’utilisateur dont le compte est actif dans Metamask. (Pour l’exemple, il faut que ce compte soit le même que celui qui utilisé par la Cloud Function)

Elle va afficher la liste des fichiers notarisés. Pour chaque fichier, on affichera également un formulaire permettant de sélectionner un fichier local et un bouton qui va lancer le calcul du hash de ce fichier et la comparaison avec le hash stocké dans la blockchain.

Pour ça, 2 fonctions sont définies :

  • sendFileForHash qui récupère le fichier sélectionné et envoie son contenu à la websocket qui va retourner son hash
  • callCheckFile qui va appeler le smart contract pour vérifier la concordance des hashs et afficher le résultat.
<!DOCTYPE html>
<html lang="fr">

<head>
    <meta charset="utf-8">
    <title>Ethereum File Notary</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/babel-polyfill/dist/polyfill.min.js"></script>

    <!-- blockchain framework -->
    <script type="text/javascript" src="./web3.min.js"></script>

    <!-- blockchain scripts -->
    <script type="text/javascript" src="./blockchain.js"></script>

</head>

<body>
	<script>
	
		/**
		* Init connections and display data when page is loaded.
		*/
		window.addEventListener('load', async() => {
			/* Create connection to blockchain and create smart contract object*/
			await initBlockchain();
			
			/* Display data read from blockchain */
			displayBlockchainInfo();
			displayFiles(ethereum.selectedAddress);
		});
		
		/**
		* Get a file from form and open websocket to get the hash.
		* @param formFileId : HTML id of form file input
		* @return an array with file name as first element, websocket as second element.
		*/
		function sendFileForHash(formFileId) {
			let file = $(formFileId)[0].files[0];
			if(!file) return;
			
			var ws = new WebSocket("ws://localhost:8100");
			var fileName = file.name;
			
			ws.onopen = function (event) {
			
				console.log("fileName : "+fileName);
				
				let reader = new FileReader();
				reader.readAsArrayBuffer(file);
				
				reader.onload = function() {
					console.log(reader.result);
					ws.send(reader.result);
				};
				
				reader.onerror = function() {
					console.log(reader.error);
				};
			};
			return [fileName, ws];
		}
		
		/**
		* Call check service
		*/
		function callCheckFile(fileId) {
			var result = sendFileForHash('#file'+fileId);
			var fileName = result[0];
			var ws = result[1];
			ws.onmessage=function(event) {
				console.log("hash : "+event.data);
				checkFile(ethereum.selectedAddress, fileId, fileName, event.data);
				ws.close();
			};
		}
	
	</script>

<div class="container-fluid">
    <div class="row">
        <div class="col-md-12">
            <div class="card">
                <h5 class="card-header">Blockchain info</h5>
                <div class="card-body">
                    Node info : <span id="nodeInfo"></span>
                    <br/>
                    Block number : <span id="blockNumber"></span>
                    <br/>
                    Account : <span id="account"></span>
                    <br/>
                    Account balance : <span id="balance"></span> ETH
                    <br/>
                    Contract balance : <span id="contractBalance"></span> ETH
                </div>
            </div>
        </div>
    </div>

    <br/>

    <div class="row">
        <div class="col-md-12">
            <div class="card">
                <h5 class="card-header">My notarized files</h5>
                <div class="card-body">
                    <div id="myFiles"></div>
                </div>
            </div>
        </div>
    </div>

</div>

</body>

</html>

Connexion à la blockchain via Metamask

Dans blockchain.js, on retrouve la même fonction de connexion à la blockchain que dans la Cloud Function sauf que cette fois, on se connecte via Metamask et non Infura. (N’oubliez pas d’indiquer l’adresse du smart contract et son ABI)

// address of smart contract
const contractAddress = "0x.....";

// ABI of smart contract
const notaryABI = [...];

// contract global object
var notaryContract;

/**
* Create a Web3 object to connect to blockchain
*/
async function initBlockchain() {
	// Modern dapp browsers...
	if (window.ethereum) {
		ethereum.autoRefreshOnNetworkChange = true;
		window.web3 = new Web3(ethereum);
		try {
			// Request account access if needed
			await ethereum.enable();
			console.log("Ethereum enabled with account : "+ethereum.selectedAddress);
		} catch (error) {
			console.error("Access denied for metamask by user");
		}

		ethereum.on("accountsChanged", (accounts) => { document.location.reload(true); });

	}
	// Legacy dapp browsers...
	else if (window.web3) {
		window.web3 = new Web3(web3.currentProvider);
	}
	// Non-dapp browsers...
	else {
		console.log('Non-Ethereum browser detected. You should consider trying MetaMask!');
	}
	
	console.log("Load contract at : "+contractAddress);
	try {
		notaryContract =  new web3.eth.Contract(notaryABI, contractAddress);
	}
	catch(error) {
		console.error("Error loading contract : "+error);
	}	
}

Récupération des fichiers notarisés

Encore dans blockchain.js, la fonction displayFiles interroge le smart contract pour récupérer tous les fichiers notarisés associés à l’adresse active dans Metamask et les affiche dans la page HTML avec le formulaire de vérification.

Elle va simplement construire une chaîne HTML qui contient le tableau qui liste les fichiers et un formulaire de vérification par fichier. Chaque fichier est identifié par son id dans le tableau du smart contract, répercuté dans la page HTML pour construire un identifiant de balise « file »+id

Elle prend en paramètre l’adresse publique à partir de laquelle récupérer les fichiers. Donc ici, on lui passera la valeur du compte actif dans Metamask qui est celui qui a été utilisé dans la Cloud Function.

/**
* Read and display notarized files from blockchain
*/
async function displayFiles(account) {
	console.log("Display files for "+account);

	notaryContract.methods.getFilesByAddress(account).call()
	.then( (files) => {
		let htmlFiles = "<table class='table'>";
		htmlFiles += "<tr><th>#</th><th>Name</th><th>Date</th><th>Check</th></tr>";
		files.forEach(function(item, index, array) {
			htmlFiles += "<tr><td>"+item.id+"</td><td>"+item.fileName+"</td><td>"+(new Date(item.time*1000)).toUTCString()+"</td><td>";
			htmlFiles += "<form method='post' action='javascript:callCheckFile("+item.id+")' enctype='multipart/form-data'><input type='file' name='fileToCheck' id='file"+item.id+"'><input type='hidden' name='id' value='"+item.id+"'><button type='submit' class='btn btn-primary'>Check</button></form>";
			htmlFiles +=" <div id='result"+item.id+"'></div></td></tr>";
		});
		htmlFiles += "</table>";
		
		$('#myFiles').html(htmlFiles);
	})
	.catch( (error) => {
	console.error("Error reading name : "+error);
	});
}

Vérification d’un fichier

Toujours dans blockchain.js, la fonction checkFile appelle la méthode de vérification du smart contract avec un nom et un hash de fichier, et l’id du fichier notarisé correspondant. Elle affiche le résultat dans la page HTML.

async function checkFile(account, id, fileName, fileHash) {
    console.log(account + " checks "+fileName+" : "+fileHash+".");

    var result =  await notaryContract.methods.checkFile(id, fileHash, fileName).call({from: account});
    console.log(result);
    if(result)
        $('#result'+id).html('<div class="alert alert-success">Untouched file!</div>');
     else
        $('#result'+id).html('<div class="alert alert-danger">Corrupted file!</div>');
}

#6 – Test

Nous pouvons maintenant tester tout ça.

1) Charger un fichier dans le bucket

Chargement du fichier dans le bucket
Chargement du fichier dans le bucket

2) Vérifier l’exécution de la Cloud Function

Dans la Cloud Function, accédez au journal, on doit voir les trace du déclenchement.

Vérification de l'exécution
Vérification de l’exécution

Attention, il se peut que la génération de la transaction et sa validation par le réseau prennent un certain temps.

La Cloud Function peut afficher la fin de son exécution, les éléments asynchrones continuent d’être à l’écoute de la transaction en arrière plan jusqu’à sa validation dans un bloc.

3) Affichage de la web app et test

Lancez l’application : node index.js

Ouvrez votre navigateur sur http://localhost:3000.

Votre navigateur doit avoir l’extension Metamask. Le compte ayant servit à faire la notarisation doit être présent dans Metamask et être le compte actif.

La page web demandera l’autorisation à Metamask de se connecter. Il faudra ensuite sélectionner le compte qui a servi à créer les notarisations dans la Cloud Function.

La page web doit s’afficher.

Interface web de vérification des fichiers
Interface web de vérification des fichiers

Sélectionnez sur votre poste le fichier que vous avez téléchargé sur Storage et cliquez sur « Check », le message de confirmation d’intégrité apparaît.

Vérification effectuée avec succès
Vérification effectuée avec succès

Maintenant, modifiez le contenu de votre fichier et recommencez l’opération. Cette fois, un message d’erreur apparaît. Le fichier a été modifié et n’est plus le même que celui déposé dans Storage.

Fichier corrompu
Fichier corrompu

Et voilà, nos fichiers sont notarisés !

La solution est fonctionnelle de façon basique, mais il reste encore des possibilités d’améliorations pour la rendre plus pertinente.

Évolutions possibles

Pour fiabiliser la notarisation, une évolution possible serait de permettre d’identifier l’utilisateur qui dépose le fichier. Il faudrait passer par un wallet type Metamask pour identifier l’utilisateur dans son navigateur et déclencher les transactions avec ses clés privées.

De même, dans notre exemple, pour que la vérification d’intégrité puisse se faire, l’utilisateur doit se connecter avec le compte ayant servit à faire la notarisation. Pouvoir identifier chaque utilisateur permettra de rendre plus flexible la vérification de ses propres fichiers. C’est aussi une étape vers un vrai système basé sur une identité décentralisée ou un KYC.

Dans l’application de vérification, on pourra ajouter la possibilité de notariser un fichier. Soit en générant le hash de la même façon que pour la vérification puis en appelant la fonction de notarisation du smart contract, soit en téléchargeant un fichier directement sur Cloud Storage. La notarisation directe depuis l’appli web, en utilisant Metamask, permettra d’identifier précisément l’utilisateur qui notarise un fichier.

Cet exemple ne prend pas en compte les mises à jour ou versions de fichier. Si on notarise à nouveau le même fichier, il sera considéré comme un nouveau fichier notarisé. On pourrait donc ajouter une gestion des versions.

Conclusion

Nous avons illustré avec cet exemple un des cas d’utilisation principaux de la blockchain. En quelques lignes de code, et avec les outils disponibles, il est possible de rapidement réaliser un petit prototype.

Il ne reste plus maintenant qu’à le tester et le passer à l’échelle dans votre organisation !

Vous retrouverez les codes source présentés dans ce tutoriel sur le repository Github

Liens