javascript

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.

Convertir des marque-pages JSON en HTML Netscape

Lorsque je formate ou que je change d'ordinateur, je fais une sauvegarde de mes marque-pages. C'est normal. Depuis plus de cinq ans, j'ai donc des fichiers de sauvegarde qui s'entassent. Certains sont au format HTML Netscape, un standard. Mais d'autres sont au format JSON. Peu commodes à utiliser, je souhaitais les convertir en HTML. Malheur à moi !

Le nouvel Opera n'importe que des fichiers HTML. Firefox m'explique que le fichier n'est pas bon. Bien, tant pis, je me lance dans la conception d'un petit script pour convertir ces JSON en HTML. Et quoi de mieux que Javascript pour faire le job ?


<script type="text/javascript">
function parseObject(obj, result) {
	result += "\n";

	if(obj.constructor == Object) { 
		// Folders
		if(obj.hasOwnProperty("children")) {
			result += "<DT><H3 ";
			var tmp = new Object();

			for(var p in obj) {
				if(p === "name" || p === "title") {
					tmp["value"] = obj[p];
				} else if(p === "dateAdded" || p === "date_added") {
					tmp["date"] = obj[p];
				} else if(p === "lastModified" || p === "date_modified") {
					tmp["modified"] = obj[p];
				}
			}

			// WebKit doesn't know how to use POSIX time è.é
			// tmp["date"] = (tmp["date"]/1000000-11644473600);
			// tmp["modified"] = (tmp["modified"]/1000000-11644473600);

			result += 'ADD_DATE="' + String(tmp["date"]).substring(0,10) + '" ';
			result += 'LAST_MODIFIED="' + String(tmp["modified"]).substring(0,10) + '">';
			result += tmp["value"] + "</H3>\n";
			result += parseObject(obj["children"], "\n");

		}	
		// Links
		else if(obj.hasOwnProperty("url") || obj.hasOwnProperty("uri")){
			result += '<DT><A HREF="';
			var tmp = new Object();

			for(var p in obj) {
				if(p === "uri" || p === "url") {
					tmp["url"] = obj[p];
				} else if(p === "name" || p === "title") {
					tmp["value"] = obj[p];
				} else if(p === "dateAdded" || p === "date_added") {
					tmp["date"] = obj[p];
				} else if(p === "lastModified" || p === "date_modified") {
					tmp["modified"] = obj[p];
				} else if(p === "charset") {
					tmp["charset"] = obj[p];
				} else if(p === "annos") {
					tmp["description"] = obj[p][0].value;
				} 
			}

			// WebKit doesn't know how to use POSIX time è.é
			// tmp["date"] = (tmp["date"]/1000000-11644473600);
			// tmp["modified"] = (tmp["modified"]/1000000-11644473600);

			result += encodeURI(tmp["url"]) + '" ADD_DATE="' + String(tmp["date"]).substring(0,10) + '" LAST_MODIFIED="';
			result += String(tmp["modified"]).substring(0,10) + '" LAST_CHARSET="' + tmp["charset"] + '" >'; 
			result += tmp["value"]+ "</A>\n";
			if(tmp.hasOwnProperty("description"))
				result += "<DD>" + tmp["description"] + "\n";
		}
		// Others
		else {
			for(var p in obj) {
				result += parseObject(obj[p], "\n");
			}
		}
	} else if(obj.constructor == Array) {
		for(var p in obj) {
			result += parseObject(obj[p], "\n");
		}	
	} 

	return result;
}


function jsonToHTML(input){
	var output = "<!DOCTYPE NETSCAPE-Bookmark-file-1>\n";
	output += '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">\n';
	output += "<TITLE>Bookmarks</TITLE>\n"; 
	output += "<H1>Bookmarks</H1>\n";
	output += "<DL>\n";
	
	// We construct a valid JSON, thanks to Firefox's shitty code è.é
	input = input.replace(",]", "]");
	input = input.replace(",}", "}");
	input = input.replace("[,", "[");
	input = input.replace("{,", "{");
	
	var data = JSON.parse(input);
	console.log(data);
	output += parseObject(data, "\n");
	
	output += "</DL>\n";
	output += "</DL>";
	return output;
}
	
	// Usage : jsonToHTML(someInputFromATextarea);
</script>

Voir une démo - Voir le dépôt GitHub

Il s'avère que j'ai trouvé pourquoi Firefox me retourne une erreur avec ces fichiers. Au départ il les exportait avec un léger défaut : en fin de fichier, il inscrivait ,]}, ce qui, bien entendu, n'est pas valide. Je ne sais pas quand le tir a été corrigé, mais dorénavant les vieux fichiers de sauvegarde sont incompatibles…

Oh, et tant que j'y suis… Chrome et dérivés (Opera, Chromium…) tendent à ne pas savoir calculer un timestamp Unix correct. J'ai d'abord pensé qu'ils ajoutaient systématiquement un 0 après les deux premiers chiffres. Un simple ajout javascript aurait alors fait l'affaire :


tmp["date"] = String(tmp["date"]).slice(0,2).concat(String(tmp["date"]).slice(3));
tmp["modified"] = String(tmp["modified"]).slice(0,2).concat(String(tmp["modified"]).slice(3));

Mais en réalité ils vont plus loin dans leur incompétence : les trois premiers chiffres sont toujours 130 !

Intrigué (l'export en HTML donnant un timestamp correct), j'ai voulu creuser un peu plus. Il s'avère que Chrome n'utilise pas un timestamp Unix Epoch mais ce qui est communément appelé webkit timestamp, un format 64 bit qu'il partage avec Windows. Au lieu de compter les secondes depuis 1970, il compte les microsecondes depuis 1601. Pourquoi 1601 ? Oh, sans doute le fameux Ballmer Peak

Tout ce qu'il y a à faire, c'est de diviser par un million pour avoir les secondes, puis de soustraire le nombre de secondes entre 1970 et 1601 (soit 11644473600, ne me remerciez pas). Soit le code :


tmp["date"] = (tmp["date"]/1000000-11644473600);
tmp["modified"] = (tmp["modified"]/1000000-11644473600);

CipherSaber.js

Monsieur Urvoas souhaite pouvoir empêcher le chiffrement, à ce qu'il en ressort des débats pour le ‪#‎PJLRenseignement‬. Le problème, c'est qu'il est impossible d'arrêter le chiffrement de données.

CipherSaber en est l'exemple parfait. Cet algorithme est si simple que n'importe quel développeur amateur peut en faire une implémentation de tête. Il se base sur l'idée qu'un Jedi devait fabriquer son propre sabre laser avant d'être admis comme chevalier, le rendant indépendant de tout Empire totalitaire. Voici une implémentation en javascript de mon cru, très basique, que vous pouvez tester sur JSFiddle ou bien en suivant le lien de la démo.

Dépôt github : https://github.com/yomli/ciphersaber.js/

Un lien « Retour en haut » dynamique

Les petits boutons « Retour en haut », on les aime tous. Généralement, il s'agit d'une simple div en position: fixed. En voici un qui n'apparait qu'arrivé à un certain pourcentage de la page, et qui s'arrêtre gracieusement juste au-dessus du footer.

Le code HTML est on ne peut plus simple :


<div class="relative">
	<div class="toTop">
		<a href="#top" title="Retour en haut">
			Retour en haut
		</a>
	</div>
</div>
<footer id="footer">
…
</footer>

Le code CSS :


.relative {
	position:relative;
}

.toTop {
	display:none;
	position:fixed;
	right:20px;
	bottom:20px;
	z-index:1000;
}

Voilà, nous avons un lien, mais il n'apparait pas pour le moment. On ajoute un peu de jQuery :


function toTop(element,footer){
	var scroll = $(window).scrollTop();
	var maxScroll = $(window).height() * 0.4;

	if(scroll > maxScroll)
		$(element).show();
	else
		$(element).hide();


	if($(element).offset().top + $(element).height() >= $(footer).offset().top - 10)
		$(element).css('position', 'absolute');
	if($(document).scrollTop() + window.innerHeight < $(footer).offset().top)
		$(element).css('position', 'fixed');
}	

$(window).scroll(function() {
	toTop('.toTop',"#footer");
});

$(window).resize(function() {
	toTop('.toTop',"#footer");
});

Scrolling doux vers une ancre

Un petit bout de jQuery intéressant pour animer le scrolling vers une ancre (ce que les anglophones appellent smooth scroll) :


$('a[href*=#]').on('click', function(event){     
	event.preventDefault();
	$('html,body').animate({scrollTop:$(this.hash).offset().top}, 700);
});	
	

Oubliez votre lien d'administration avec Javascript !

Parfois, il faut quelque chose de simple. Accéder à l'interface d'administration d'un CMS devrait être aussi simple que de cliquer sur un bouton. En fait, c'est déjà le cas dans plusieurs CMS, il suffit effectivement de cliquer sur le beau lien Administration.

Mais c'est peu élégant. C'est pourquoi beaucoup de gens décident de garder le lien dans leurs favoris. C'est bien, mais pour peu que vous changiez de navigateur ou décidiez de poster depuis l'ordinateur de votre belle-tante, retrouver le lien d'administration devient compliqué.

C'est pourquoi ce petit bout de Javascript vous permettra d'accéder à l'administration en tapant simplement trois fois d'affilée la touche Entrée. Il s'agit de Javascript vanilla, mais j'ai mis en commentaire la ligne à changer pour le rendre jQuery :


function keyboardAuth(redirection) {
	var enterCount = 0;
	
	//$(document).keypress(function(event){
	document.addEventListener("keypress", function(event) {
		var keycode = (event.keyCode ? event.keyCode : event.which);
		if(keycode == '13'){
			enterCount++;
			if(enterCount >= 3){
				enterCount = 0;
				window.location = redirection;
			}
		setTimeout(function(){ enterCount = 0; },2000);
		}

	});
}
	

Notez que vous n'aurez que 2 secondes pour appuyer trois fois sur la touche Entrée, ceci pour éviter que quelqu'un écrivant un commentaire ne soit redirigé vers la page d'administration pour avoir simplement voulu mettre en page son commentaire…

Tout ce que vous aurez à faire ensuite, c'est appeler cette fonction avec le lien de votre interface d'administration :


keyboardAuth("/admin/");
	
Haut de page