Comment utiliser WSSE dans une appli Android

Share Button

Read the English version

Après avoir publié un article sur la configuration WSSE dans Symfony, nous avons reçu beaucoup de questions sur l’utilisation de WSSE dans une application Android. Nous espérons que cet article répondra à ces questions!
Notez que nous ne parlerons que WSSE et que nous n’aborderons pas la conception d’un client rest dans une application Android.

Ce dont vous avez besoin

L’authentification WSSE est basées sur 4 points essentiels :

  • un identifiant
  • un nonce (un nombre cryptographique généré pour éviter les attaques par rejeu)
  • le timestamp actuel dans un format donné
  • le digest du mot de passe

3 peuvent être facilement obtenus :

  • L’identifiant est fourni par l’utilisateur lui-même via un champ input texte lors de l’authentification (souvent un formulaire de login). Aucun problème pour un développeur Android
  • Le nonce est généré, vous devez juste trouver le bon bout de code (mais comme c’est votre jour de chance, je vais le mettre un peu plus bas).
  • Le timestamp actuel : no comment :)

Comment générer le digest

Pour générer le digest, il y a un peu plus de boulot.

L’utilisateur va entrer un mot de passe en clair dans un champ texte. Votre appli Android va devoir le hasher et le stocker, par exemple dans une base sqlite. La chose la plus importante à garder à l’esprit est qu’une partie du mécanisme d’authentification de WSSE est basée sur la comparaison de deux mots de passe hashés (l’un donné par le client, l’autre déjà stocké par le serveur).

En conséquence, le mot de passe hashé de votre appli Android DOIT ÊTRE exactement le même que celui créé et stocké par Symfony (ou autre chose) côté serveur.

Par défaut, Symfony utilise l’algorithme de hashage SHA-512 avec un mot de passe salé (le salage est une chaine aléatoire) et utilise 5000 itérations.

Vous devez effectuer la même opération dans votre application Android mais, pour faire cela, vous devrez connaitre le salage spécifique à cet utilisateur. La seule solution que nous avons trouvée est d’exposer publiquement le salage de l’utilisateur dans l’API rest (après quelques recherches, nous pensons que ça n’engendre aucune faille de sécurité :
discussion sur le salage sur Stackoverflow (en anglais)).

Par exemple, vous pouvez avoir une ressource publique comme http://yoursite.com/api/users/aniceusername/infos qui retourne des informations publiques sur l’utilisateur en json (ce qui inclut le salage évidemment), comme ceci :

{"username":"jb","salt":"e2l8uk8xhm0oo0c4ss4goooo0kc400w","usedName":"Jb IDZIK","firstname":"Jb","latitude":50.543009,"longitude":2.9833120000001,"gender":"M","avatar":"jb.png"}

Lorsque vous aurez récupéré le salage, vous pourrez commencer à hasher le mot de passe. Assez bavardé, codons un peu.
Voici une méthode pour le faire :

public String hashPassword(String salt, String clearPassword) {
	String hash = "";
	try {
		//Log.d("AuthProvider", "start hashing password...");
		String salted = null;
		if(salt == null || "".equals(salt)) {
			salted = clearPassword;
		} else {
			salted = clearPassword + "{" + salt + "}";
		}
		MessageDigest md = MessageDigest.getInstance("SHA-512");
		byte sha[] = md.digest(salted.getBytes());
		for(int i = 1; i < 5000; i++) {
			byte c[] = new byte[sha.length + salted.getBytes().length];
			System.arraycopy(sha, 0, c, 0, sha.length);
			System.arraycopy(salted.getBytes(), 0, c, sha.length, salted.getBytes().length);
			sha = md.digest(c);
		}
		hash = new String(Base64.encode(sha,Base64.NO_WRAP));
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
		//do something with this exception
	}
	//Log.d("AuthProvider", "hashing password is done!");
	return hash;
}

Et voilà une bonne partie de faite. Si vous pensez qu’itérer 5000 fois est sale, vous devrez mettre les mains dans Symfony pour changer ça.

Générer les bons entêtes WSSE

A ce stage, nous avons tout ce qu’il faut pour créer la requête HTTP avec authentification WSSE.
Nous avons juste besoin de méthodes pour générer les bons entêtes WSSE pour nous. Voici une classe complète qui fait cela :

public class WsseToken {	
	public static final String HEADER_AUTHORIZATION = "Authorization";
	public static final String HEADER_WSSE = "X-WSSE";
	
	private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
	//in our case, User is an entity (just a POJO) persisted into sqlite database
	private User user;
	private String nonce;
	private String createdAt;
	private String digest;

	public WsseToken(User user) {
		//we need the user object because we need his username
		this.user = user;
		this.createdAt = generateTimestamp();
		this.nonce = generateNonce();
		this.digest = generateDigest();
	}

	private String generateNonce() {
		SecureRandom random = new SecureRandom();
		byte seed[] = random.generateSeed(10);
		return bytesToHex(seed);
	}
	
	public static String bytesToHex(byte[] bytes) {
	 final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
	 char[] hexChars = new char[bytes.length * 2];
	 int v;
	 for ( int j = 0; j < bytes.length; j++ ) {
	 v = bytes[j] & 0xFF;
	 hexChars[j * 2] = hexArray[v >>> 4];
	 hexChars[j * 2 + 1] = hexArray[v & 0x0F];
	 }
	 return new String(hexChars);
	}

	private String generateTimestamp() {
		sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
		return sdf.format(new Date());
	}

	private String generateDigest() {
		String digest = null;
		try {
			MessageDigest md = MessageDigest.getInstance("SHA-1");
			StringBuilder sb = new StringBuilder();
			sb.append(this.nonce);
			sb.append(this.createdAt);
			sb.append(this.user.getPassword());
			byte sha[] = md.digest(sb.toString().getBytes());
			digest = Base64.encodeToString(sha,Base64.NO_WRAP);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return digest;
	}

	public String getWsseHeader() {
		StringBuilder header = new StringBuilder();
		header.append("UsernameToken Username=\"");
		header.append(this.user.getUsername());
		header.append("\", PasswordDigest=\"");
		header.append(this.digest);
		header.append("\", Nonce=\"");
		header.append(Base64.encodeToString(this.nonce.getBytes(), Base64.NO_WRAP));
		header.append("\", Created=\"");
		header.append(this.createdAt);
		header.append("\"");
		return header.toString();
	}
	
	public String getAuthorizationHeader() {
		return "WSSE profile=\"UsernameToken\"";
	}

Créer la requête HTTP avec des entêtes WSSE

Finalement, vous devez juste appeler getWsseHeader() et getAuthorizationHeader() pour obtenir les entêtes WSSE.
Voici un exemple (très simplifié) pour construire une méthode HTTP get dans une application Android et utilisez la méthode précédente pour récupérer les entêtes.

URL url = new URL(yourStringurl);
HttpURLConnection httpCon = (HttpURLConnection) url.openConnection();
httpCon.setRequestMethod("GET");
httpCon.setRequestProperty("Content-Type", "application/json");
httpCon.setConnectTimeout(5000);
httpCon.setReadTimeout(5000);
// ici, vous utilisez la classe WsseToken class pour récupérer les entêtes WSSE
WsseToken token = new WsseToken(user);
this.httpConnection.setRequestProperty(WsseToken.HEADER_AUTHORIZATION, token.getAuthorizationHeader());
this.httpConnection.setRequestProperty(WsseToken.HEADER_WSSE, token.getWsseHeader());

On espère que ça vous aider. Maintenant, amusez-vous!

Share Button

Comments are closed.