Lors de l'intégration d'API REST sécurisées, il est fréquent que les serveurs renvoient des données chiffrées. L'utilisation du convertisseur standard converter-gson de Retrofit échoue dans ce scénario, car il s'attend à recevoir du JSON en clair. Pour contourner ce problème, il est nécessaire de créer un ConverterFactory sur mesure capable de déchiffrer la réponse avant de la désérialiser, et inversement pour les requêtes.
Configuration du Retrofit Builder
L'écosystème Android utilise couramment la combinaison Retrofit, OkHttp et Gson. Lorsque les flux de données sont obfusqués ou chiffrés, le parseur JSON natif ne peut pas interpréter la chaîne de caractères brute. La soultion consiste à intercepter le flux via une fabrique de convertisseurs personnalisée et à l'enregistrer dans le builder de Retrofit.
// Enregistrement du convertisseur sécurisé
Retrofit secureClient = new Retrofit.Builder()
.baseUrl("https://api.exemple.com/")
.addConverterFactory(SecureJsonConverterFactory.create())
.build();
Implémentation de la fabrique de convertisseurs
La classe fabrique doit hériter de Converter.Factory. Son rôle est de fournir les convertisseurs appropriés pour les corps de requête et de réponse en instanciant nos classes personnalisées.
public class SecureJsonConverterFactory extends Converter.Factory {
private final Gson gsonInstance;
public static SecureJsonConverterFactory create() {
return new SecureJsonConverterFactory(new Gson());
}
private SecureJsonConverterFactory(Gson gson) {
this.gsonInstance = Objects.requireNonNull(gson, "L'instance Gson ne peut pas être nulle");
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
TypeAdapter<?> typeAdapter = gsonInstance.getAdapter(TypeToken.get(type));
return new DecryptedBodyConverter<>(gsonInstance, typeAdapter);
}
@Override
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
TypeAdapter<?> typeAdapter = gsonInstance.getAdapter(TypeToken.get(type));
return new EncryptedBodyConverter<>(gsonInstance, typeAdapter);
}
}
Déchiffrement et parsing de la réponse
Le convertisseur de réponse est responsable de la transformation du flux réseau. Il doit d'abord appliquer l'algorithme de déchiffrement (ici illustré par un décodage Base64 pour simplifier) avant de transmettre la chaîne JSON claire à Gson.
final class DecryptedBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson gson;
private final TypeAdapter<T> typeAdapter;
DecryptedBodyConverter(Gson gson, TypeAdapter<T> typeAdapter) {
this.gson = gson;
this.typeAdapter = typeAdapter;
}
@Override
public T convert(ResponseBody responseBody) throws IOException {
String rawEncryptedPayload = responseBody.string();
responseBody.close();
try {
// Application de l'algorithme de déchiffrement (exemple avec Base64)
byte[] decodedBytes = Base64.getDecoder().decode(rawEncryptedPayload);
String clearJson = new String(decodedBytes, StandardCharsets.UTF_8);
JsonReader reader = gson.newJsonReader(new StringReader(clearJson));
return typeAdapter.read(reader);
} catch (Exception e) {
throw new IOException("Échec du déchiffrement ou du parsing JSON", e);
}
}
}
Chiffrement du corps de la requête
Pour maintenir une symétrie dans la communication, le convertisseur de requête doit sérialiser l'objet en JSON, puis chiffrer le résultat avant de l'envoyer au serveur. Le type de média doit être ajusté si le serveur attend du texte brut chiffré plutôt que du JSON standard.
final class EncryptedBodyConverter<T> implements Converter<T, RequestBody> {
private static final MediaType TEXT_MEDIA_TYPE = MediaType.get("text/plain; charset=UTF-8");
private final Gson gson;
private final TypeAdapter<T> typeAdapter;
EncryptedBodyConverter(Gson gson, TypeAdapter<T> typeAdapter) {
this.gson = gson;
this.typeAdapter = typeAdapter;
}
@Override
public RequestBody convert(T value) throws IOException {
Buffer buffer = new Buffer();
try (Writer writer = new OutputStreamWriter(buffer.outputStream(), StandardCharsets.UTF_8)) {
JsonWriter jsonWriter = gson.newJsonWriter(writer);
typeAdapter.write(jsonWriter, value);
jsonWriter.close();
}
// Sérialisation en JSON puis application de l'algorithme de chiffrement
byte[] rawJsonBytes = buffer.readByteArray();
String encryptedPayload = Base64.getEncoder().encodeToString(rawJsonBytes);
return RequestBody.create(TEXT_MEDIA_TYPE, encryptedPayload);
}
}