Créer un navigateur personnalisé - 7ème partie

Intégration d'un gestionnaire de mots de passe

2 mars 2016

Introduction

Ce projet est open source et accessible depuis GitHub GL-Browser.

Cet article est le 7ème d'une série d'articles concernant la création d'un navigateur internet incluant par défaut des fonctionnalités que l'on retrouve habituellement sous forme d'extensions dans Chrome ou Firefox.

Exemples de fonctionnalités :

Nous souhaitons :

Nous utilisons Electron qui mixe le navigateur open source Chromium avec la richesse du framework Node.js.

Objectifs

Dans ce 7ème article, nous intégrons un gestionnaire de mots de passe permettant de se connecter de manière transparente et automatique à ses différents comptes utilisateur, par exemple github ou twitter.

Gestionnaire

Résumé des articles précédents

Article 1: initialisation du projet avec Electron.
Article 2: injection du CSS et du JavaScript dans la page de Google.
Article 3: injection de scripts JavaScript et de styles CSS suivant l'URL.
Article 4: bloquer les requêtes de suivi/tracking des utilisateurs.
Article 5: amélioration de l'interface utilisateur avec Bootstrap.
Article 6: encapsulation des composants visuels avec Riot.

Pour récupérer directement les sources du 7ème article :

git clone https://github.com/emmanuelroecker/GL-Browser
git checkout article7

Encapsulation des composants

Les composants visuels

Dans le précédent article, les composants visuels étaient encapsulés à l'aide de Riot.
L'ensemble de ces composants visuels sont désormais dans le répertoire components/UX du projet.

Les composants internes

Les autres composants internes sont désormais encapsulés à l'aide des classes de JavaScript 6 et définis comme des modules de node.js.

L'ensemble de ces composants internes sont dans le répertoire components du projet :

Stocker/Crypter/Décrypter les identifiants

Les identifiants de connexion (noms d'utilisateur et mots de passe) sont cryptés/décryptés avec un mot de passe principal et stockés localement dans un fichier.

Ce mot de passe principal est confidentiel, stocké nulle part et uniquement connu de l'utilisateur.

Composant de cryptage/décryptage

Le composant components/crypt/crypt.js permet de :

Utiliser la librairie crypto de node.js.

L'algorithme de cryptage utilisé est AES :

encrypt(text,password) {
    let cipher = this._crypto.createCipher('aes-256-ctr', password)
    let crypted = cipher.update(text, 'utf8', 'hex')
    crypted += cipher.final('hex');
    return crypted;
}

L'algorithme de hashage du mot de passe principal est SHA :

hash(password) {
    return this._crypto.createHash('sha256').update(password).digest('base64');
}

Crypter les identifiants de connexion

Dans l'attente d'une interface graphique dédiée à la gestion des identifiants, le hash et les identifiants cryptés sont générés à l'aide d'une ligne de commande.

Pour gérer l'interface en ligne de commande, la librairie commander est ajoutée au fichier package.json :

"devDependencies": {
    "electron-prebuilt": "^0.36.0",
    "js-yaml": "^3.5.0",
    "match-pattern": "^0.0.1",
    "bootstrap": "^3.3.0",
    "jquery": "^1.9.1",
    "riot": "^2.3.0",
    "commander": "^2.9.0"
  }

La nouvelle librairie est installée en ligne de commande avec :

npm update

Le fichier command/crypt.js est exécutable en ligne de commande :

const crypt = require('../components/crypt/crypt.js'); //utilise le composant crypt créé plus haut

const program = require('commander');
program
    .version('0.0.1')
    .option('-u, --username [username]','username to encrypt')
    .option('-p, --userpassword [userpassword]', 'user password to encrypt')
    .option('-P, --masterpassword [masterpassword]', 'masterpassword used to encrypt')
    .parse(process.argv);

let usernameCrypted = crypt.encrypt(program.username,program.masterpassword);
let userpasswordCrypted = crypt.encrypt(program.userpassword, program.masterpassword);
let masterpasswordHashed = crypt.hash(program.masterpassword);
console.log('Username Crypted: ' + usernameCrypted);
console.log('UserPassword Crypted: ' + userpasswordCrypted);
console.log('MasterPassword Hashed: ' + masterpasswordHashed);

Pour crypter, lancer en ligne de commande à partir du répertoire du projet :

node command/crypt.js -u [nom d'utilisateur] -p [mot de passe utilisateur] -P [mot de passe principal]

Cela affiche :

Username Crypted: [nom d'utilisateur crypté]
UserPassword Crypted: [mot de passe utilisateur crypté]
MasterPassword Hashed: [mot de passe principal hashé]

Ces valeurs doivent ensuite être copiées dans le fichier components/autologin/autologin.yml décrit ci-dessous.

Stocker les identifiants cryptés dans un fichier local

Le fichier components/autologin/autologin.yml au format YAML contient l'ensemble des identifiants :

- hash: [clé de hash du mot de passe principal]
- name: twitter
  login: [nom d'utilisateur crypté]
  password: [mot de passe crypté]
  patterns:
           - "*://*.twitter.com/*"
- name: github
  login: [nom d'utilisateur crypté]
  password: [mot de passe crypté]
  patterns:
           - "*://*.github.com/*"
- ...

Le champ patterns contient une liste de match patterns d'URL pour lesquels s'appliquent les identifiants associés (voir Article 4).

Scripts de connexion automatique à Twitter et GitHub

Chaque site internet a sa propre interface d'identification, il est donc nécessaire de réaliser un script spécifique pour chaque site.

Pour éviter tout problème de compatibilité avec la page internet affichée, le code injecté doit être en JavaScript natif (sans utiliser de librairies tierces).

sessionStorage est utilisé pour empêcher les tentatives de connexion infinies.

Twitter

Le fichier components/autologin/twitter/autologin.js contient le script spécifique de connexion automatique à Twitter :

//évènement déclenché lors d'une demande d'autoconnexion de l'utilisateur
ipcRenderer.on('login', function (event, user) {
    //user contient le nom d'utilisateur et le mot de passe décryptés

    //vérifie si une tentative de connexion a déjà été effectuée
    if (sessionStorage.getItem('glAutologin'))
        return;

    //vérifie que l'utilisateur est déconnecté
    let body = document.getElementsByTagName('body')[0];
    if (!(body.classList.contains('logged-out'))) {
        return;
    }

    //récupère les différents champs du formulaire
    let loginform = document.getElementsByClassName('LoginForm')[0];
    let username = loginform.getElementsByClassName('LoginForm-username')[0].getElementsByTagName('input')[0];
    let password = loginform.getElementsByClassName('LoginForm-password')[0].getElementsByTagName('input')[0];

    //remplit le formulaire
    username.value = user.login;
    password.value = user.password;
    loginform.submit(); //envoie la demande de connexion au serveur
    username.value = "";
    password.value = "";
    sessionStorage.setItem('glAutologin', true);
});

GitHub

De la même manière que pour Twitter, le fichier components/autologin/github/autologin.js contient le script de connexion automatique à GitHub :

//évènement déclenché lors d'une demande d'autoconnexion de l'utilisateur
ipcRenderer.on('login', function (event, user) {
    //user contient le nom d'utilisateur et le mot de passe décryptés

    //vérifie si une tentative de connexion a déjà été effectuée
    if (sessionStorage.getItem('glAutologin'))
        return;

    //vérifie que l'utilisateur est déconnecté
    let body = document.getElementsByTagName('body')[0];
    if (!body.classList.contains('logged_out'))
        return;

    //charge la page login si ce n'est pas la page courante
    if (window.location.pathname != "/login") {
        window.location.href = "/login";
        return;
    }

    //récupère les différents champs du formulaire
    let loginform = document.getElementsByTagName('form')[0];
    let username = document.getElementById('login_field');
    let password = document.getElementById('password');

    //remplit le formulaire
    username.value = user.login;
    password.value = user.password;
    loginform.submit(); //envoie la demande de connexion au serveur
    username.value = "";
    password.value = "";

    sessionStorage.setItem('glAutologin', true);
});

Composants autologin

Composant interne

Le composant interne components/autologin/autologin.js permet :

Le nom d'utilisateur et le mot de passe sont décryptés au moment de la demande de connexion automatique par l'utilisateur.
Pour des raisons de sécurité, les identifiants décryptés sont temporairement transférés dans la page visitée à l'aide des communications IPC d'Electron.

webview.send('login',login,password);

Pour avoir accès à ce mécanisme de communication entre la page visitée et le composant autologin, il est nécessaire d'avoir recours au preload.

Le fichier preloadWebview.js contient le code JavaScript à exécuter avant tout script de la page visitée :

global.ipcRenderer = require('electron').ipcRenderer; //création de la variable globale de communication IPC

Pour permettre au webview du composant page.riot.tag de charger ce script :

<webview class="gl-webview" preload="./components/preloadWebview.js">
</webview>

Composant visuel

Le nouveau composant Riot autologin.riot.tag permet à l'utilisateur :

<autologin>
  <div id="dropdown" class="dropdown pull-right">
    <a class="dropdown-toggle" data-toggle="dropdown" role="button">
      <span id="dropdownicon" class="glyphicon glyphicon-lock text-danger"></span>
    </a>
    <div class="dropdown-menu">
      <form>
        <div class="form-group">
          <label for="password" class="sr-only">Mot de passe</label>
          <input id="password" onkeypress={keypress} class="form-control" type="password" placeholder="Mot de passe"></input>
        </div>
        <span id="message" class="text-danger"></span>
        <button type="button" class="btn btn-default btn-block" onclick={login}>Connexion</button>
      </form>
    </div>
  </div>

  <style scoped>
    .dropdown-menu {
       padding: 5px 5px 5px;
    }
  </style>

  <script>
    'use strict';

    autologin() {
      //transfère au composant interne autologin le mot de passe principal et vérifie si il est valide
      if (!autologin.setMasterPassword(this.password.value)) {
        this.message.textContent = 'Bad password'; //affiche un message d'erreur
      } else {
        this.dropdownicon.classList.remove('text-danger'); //modifie la couleur de l'icône en bleu
        this.dropdown.classList.remove('open'); //ferme le menu
      }
      this.password.value = "";
    }

    //appelée à chaque fois qu'un caractère est saisi
    keypress(e) {
      this.message.textContent = '';    //efface le champ d'erreur
      if (e.which !== 13) {
        return true;
      }
      //active le mot de passe principal lorsque la touche entrée est saisie
      this.autologin();
      return false;
    }

    //active le mot de passe principal lors d'un clique sur le bouton de validation
    login(e) {
      this.autologin();
    }
  </script>
</autologin>

Ce nouveau composant est ajouté à index.html :

<body>
  <div>
    <autologin></autologin>
    <tabs>
    </tabs>
  </div>
</body>

<script src="./components/UX/autologin.riot.tag" type="riot/tag"></script>

Résultat

Ci-dessous la nouvelle architecture du projet :

Gestionnaire

Prochain article

Le prochain article traitera de l'intégration d'un gestionnaire de favoris et de l'autocomplétion des URL saisies par l'utilisateur.