Chiffrer un message avec OpenPGP.js

Pour ma page de contact, je souhaitais faciliter l’envoi de messages chiffrés avec ma clé publique PGP. Dans l’idéal, une simple case à cocher permettrait de chiffrer le message avant l’envoi au serveur, puis de là dans ma boite mail. C’est pourquoi je me suis penché sur plusieurs implémentations d’OpenPGP en Javascript.

La première, Hanewinkel, était assez légère une fois minifiée (30 ko). Néanmoins, le chiffrement de messages contenant des caractères accentués posait problème. J’aurais pu m’en contenter, mais c’est toujours bon d’arriver à lire des messages directement. Si toutefois ça vous intéresse, voici un petit snippet à intégrer :

function encryptPGP(key, message){
    var pu = new getPublicKey(key);
    if(pu.vers == -1) 
        return;

    var keytyp = 0;      // 0=RSA, 1=Elgamal
    var keyid = pu.keyid;
    var pubkey = pu.pkey.replace(/\n/g,'');
    return doEncrypt(keyid, keytyp, pubkey, message);
}

$("#contact form").on("submit", function() {

    // Comme on va peut-être avoir besoin de
    // chiffrer le message, on n'utilise pas
    // le .serialize() habituel d'Ajax, mais
    // un tableau.
    var donnees = $(this).serializeArray();

    // Si la checkbox est cochée
    if($("#contact-crypted")[0].checked) {
        // On récupère la clé publique affichée
        // dans la page
        var key = $("#pgp-key").html();
        // On récupère le message
        var message = $("#contact-message").val();
        // On le chiffre
        message = encryptPGP(key, message);
        // Et on affiche le message chiffré
        $("#contact-message").val(message);

        // Enfin, on remplace le message par
        // son équivalent chiffré dans le tableau
        for(var i = 0; i < donnees.length; i++) {
            if(donnees[i].name == "contact-message")
                donnees[i].value = message;
        }
    }

    // On transforme notre tableau en une suite
    // de paramètres dont Ajax va se charger
    donnees = jQuery.param(donnees);

    // Et on envoie tout ça par Ajax pour
    // éviter un rechargement de la page
    $.ajax({
        url: $(this).attr("action"),
        type: $(this).attr("method"), 
        data: donnees,
        success: function(html) { 
            alert(html);
        },
        error: function(html) {
            alert(html);
        }
    });

    return false;
});

Je suis donc allé voir du côté d’openpgpjs, une solution plus lourde (250 ko), mais qui cette fois fonctionne avec les caractères accentués. Le problème principal qu’on a ici, c’est que openpgp est une promise. C’est cool pour un tas de choses, mais là on aimerait bien attendre la fin du chiffrement avant d’envoyer notre message au serveur. Oh, et au cas où vous vous le demanderiez, openpgp.js embarque un polyfill pour assurer la compatibilité avec les navigateurs qui ne tiennent pas leurs promesses…

Donc je passerai sur mes envies de meurtre lorsque j’ai compris que je devrais refaire tout le code et vous le présente tel quel :

$("#contact form").on("submit", function() {

    // Si la checkbox est cochée
    if($("#contact-crypted")[0].checked) {

        var contact = $("#contact form");

        // Comme on va peut-être avoir besoin de
        // chiffrer le message, on n'utilise pas
        // le .serialize() habituel d'Ajax, mais
        // un tableau.
        var donnees = $(contact).serializeArray();

        // On récupère le message
        var message = $("#contact-message").val();

        // On récupère la clé publique affichée
        // dans la page et on la prépare
        var pubkey = openpgp.key.readArmored($("#pgp-key").html());

        // Enfin, on lance le chiffrement
        openpgp.encryptMessage(pubkey.keys, message).then(function(pgpMessage){

            // On récupère le message chiffré
            message = pgpMessage;

            // Et on l'affiche
            $("#contact-message").val(message);

            // Enfin, on remplace le message par
            // son équivalent chiffré dans le tableau
            for(var i = 0; i < donnees.length; i++) {
                if(donnees[i].name == "contact-message")
                donnees[i].value = message;
            }

            // On transforme notre tableau en une suite
            // de paramètres dont Ajax va se charger
            donnees = jQuery.param(donnees);

            // Et on envoie tout ça par Ajax pour
            // éviter un rechargement de la page
            $.ajax({
                url: $(contact).attr("action"),
                type: $(contact).attr("method"), 
                data: donnees,
                success: function(html) { 
                    alert(html);
                },
                error: function(html) {
                    alert(html);
                }
            });
        }).catch(function(error){
            alert(error);
        });
    }
    // Et là la méthode classique,
    // sans chiffrement
    else {
        $.ajax({
            url: $(this).attr("action"),
            type: $(this).attr("method"), 
            data: $(this).serialize(),
            success: function(html) { 
                alert(html);
            },
            error: function(html) {
                alert(html);
            }
        });
    }   

    return false;
});

Note sur la sécurité : ne déployez pas cette solution autrement que pour un petit formulaire de contact. Il s’agit plus d’un proof of concept. J’entends par là qu’elle se base sur ce qu’on appelle dans le milieu host based security, autrement dit aucune véritable sécurité. Il suffit à quelqu’un (que ce soit l’hébergeur, le FAI, le VPN, le navigateur, etc.) de changer la clé publique, le fichier openpgp.js ou le code javascript pour qu’il soit capable de lire le message. Plus d’informations sur cet article.