← retour au journal
publié le 26 mai 2026 · L. Benoist
#navionics#protobuf#ntf#gps#retro-ingenierie

Décoder les fichiers de tracé Navionics (.ntf / .nts)

Comment transformer un fichier « illisible » d'application de navigation en un tracé GPS exploitable — récit complet d'une rétro-ingénierie, expliqué pas à pas.

OS
Android / iOS
application
Navionics
Format
.ntf / .nts (Protocol Buffers)
Éditeur
Navionics
Sortie
GPX

À qui s’adresse cet article. À toute personne amenée à exploiter les données de l’application Navionics, même sans expérience de la rétro-ingénierie. Chaque notion technique est expliquée en langage simple avant d’être utilisée. Si vous suivez le tutoriel de bout en bout, vous obtiendrez le même résultat : le tracé GPS complet, sous une forme standard et lisible.

Avertissement. Le format de ces fichiers n’est pas documenté publiquement. Tout ce qui suit a été reconstitué par observation et analyse — ce n’est pas une spécification officielle de Navionics. Les noms internes cités proviennent de l’application elle-même. Les emplacements de fichiers peuvent varier selon les versions.


Sommaire

  1. Le problème de départ
  2. Quelques notions de base (à lire avant tout)
  3. Les fichiers de Navionics et où les trouver
  4. Étape 1 — Identifier le format
  5. Étape 2 — Comprendre comment protobuf range les données
  6. Étape 3 — Décoder le .nts (le résumé)
  7. Étape 4 — Décoder le .ntf (le tracé)
  8. Le découpage en détail — sur les vrais octets
  9. Étape 5 — Donner un sens aux données
  10. Étape 6 — Retrouver le « plan » officiel du format
  11. Étape 7 — Écrire le décodeur final
  12. La leçon : ce que chaque méthode apporte
  13. Tutoriel reproductible — toutes les commandes
  14. Annexes : outils, scripts, schéma, glossaire

1. Le problème de départ

On récupère, dans les données d’un téléphone, deux fichiers produits par l’application de navigation marine Navionics : un fichier .ntf et un fichier .nts, portant le même nom (un identifiant unique).

Ouverts dans un éditeur de texte, ils n’affichent que des caractères incompréhensibles. Impossible de les lire directement. L’objectif : en extraire le tracé GPS — la liste des positions du bateau, avec l’heure de chaque point — sous une forme exploitable (par exemple un fichier GPX, le format standard que lisent tous les logiciels de cartographie).

Cet article raconte toute la démarche, de ce fichier opaque jusqu’au tracé lisible.


2. Quelques notions de base

Avant de commencer, quelques mots de vocabulaire. Ils reviendront partout.

Un fichier « binaire ». Un fichier texte contient des lettres et des chiffres lisibles. Un fichier binaire contient des données brutes destinées à la machine, pas à l’œil humain. Ce n’est pas pour autant « chiffré » (codé secrètement) : c’est juste écrit dans un format compact que seul le bon programme sait relire.

Octet, et notation hexadécimale. Un fichier est une suite d’octets (des nombres de 0 à 255). Pour les afficher, on utilise souvent l’hexadécimal (base 16) : c’est juste une façon d’écrire ces nombres de manière compacte. 0x1a est l’écriture hexadécimale du nombre 26. L’hexadécimal n’est qu’une manière d’afficher — ce ne sont pas des données différentes.

Protocol Buffers (« protobuf »). C’est une méthode, créée par Google, pour ranger des données dans un fichier binaire de façon compacte. Beaucoup d’applications l’utilisent. Un fichier protobuf n’est pas chiffré : il est juste organisé selon des règles précises. Apprendre ces règles, c’est apprendre à le lire.

Le « schéma » (.proto). Protobuf sépare deux choses : les données (le fichier .ntf) et le plan qui décrit ces données (un fichier appelé .proto). Le plan dit : « le premier renseignement est la latitude, le deuxième la longitude… ». Sans le plan, on a les données mais pas leurs étiquettes. Une grande partie du travail consistera, justement, à reconstituer ce plan.

Rétro-ingénierie. C’est l’art de comprendre comment un système fonctionne sans en avoir la documentation, en l’observant de l’extérieur. C’est exactement ce qu’on fait ici.

Une remarque rassurante. La démarche ressemble à une enquête : on part d’indices, on forme des hypothèses, on les teste, on se trompe parfois, on corrige. Les impasses font partie du processus — on en rencontrera, et on expliquera pourquoi elles sont utiles.


3. Les fichiers de Navionics et où les trouver

Navionics manipule trois types de fichiers liés aux déplacements :

ExtensionContenuNature réelle
.ndcune route : un itinéraire planifié à l’avanceun fichier GPX (texte) simplement renommé
.ntfun tracé : les positions réellement parcouruesun fichier binaire protobuf
.ntsle résumé du tracéun fichier binaire protobuf

Distinction importante : route ≠ tracé. Une route (.ndc) est un trajet prévu, dessiné à la main avant de partir — elle ne prouve aucun déplacement réel. Un tracé (.ntf) est l’enregistrement de positions réellement relevées par le GPS pendant la navigation. Pour une analyse, les deux n’ont pas du tout la même valeur.

Le .ndc est facile : c’est du texte, lisible dans n’importe quel éditeur. Les .ntf et .nts sont le vrai sujet de cet article.

Où les trouver. Ces fichiers sont stockés dans l’espace de données réservé à l’application Navionics sur le téléphone. À titre indicatif :

  • sur Android : dans un dossier du type Android/data/ au nom de l’application ;
  • sur iPhone : dans le « conteneur » de l’application.

Chaque tracé est identifié par un nom unique (un long identifiant), et ce même nom est partagé par le trio .ndc / .ntf / .nts du même enregistrement. Les fichiers eux-mêmes ne contiennent pas de « titre » : c’est le nom de fichier qui sert d’identité.


4. Étape 1 — Identifier le format

But de l’étape : comprendre à quoi on a affaire.

Premier réflexe devant un fichier inconnu : demander à l’ordinateur de l’identifier. Sous Linux ou Mac, la commande file examine le début du fichier et annonce son type :

code
file mon_fichier.ntf

Pour notre .ntf, la réponse est décevante : data. Traduction : « je ne reconnais pas ». Beaucoup de formats laissent une signature au tout début du fichier (quelques octets caractéristiques) ; data signifie qu’il n’y en a pas. Mais attention : « non reconnu » ne veut pas dire « chiffré ».

Pour aller plus loin, on regarde le contenu brut du fichier, octet par octet. C’est ce qu’on appelle un hexdump : un affichage qui montre, côte à côte, les octets en hexadécimal et, quand c’est possible, les caractères lisibles correspondants.

code
od -A x -t x1z mon_fichier.ntf | head

Dans ce hexdump, deux choses sautent aux yeux :

  • des morceaux de texte lisible noyés dans le binaire — par exemple le nom d’un fuseau horaire comme Africa/Dar_es_Salaam ;
  • juste avant ces textes, des petits octets récurrents (08, 12, 1a…).

Ces deux indices, ensemble, sont la signature de protobuf. Le texte lisible montre qu’il n’y a pas de chiffrement (un fichier chiffré n’aurait aucun mot lisible). Et les petits octets récurrents sont la marque de fabrique du rangement protobuf.

Conclusion de l’étape 1 : le fichier n’est pas chiffré. Il est sérialisé au format Protocol Buffers. Il « suffit » donc d’apprendre à lire ce format.


5. Étape 2 — Comprendre comment protobuf range les données

But de l’étape : apprendre les règles de rangement de protobuf. C’est la seule partie un peu technique, mais une fois comprise, tout le reste découle.

L’idée générale

Un fichier protobuf est une liste de renseignements mis bout à bout. Chaque renseignement (protobuf dit un « champ ») est rangé en deux parties :

  1. une étiquette qui dit de quel renseignement il s’agit et quelle forme il a ;
  2. la valeur elle-même.

Il n’y a pas de sommaire, pas d’en-tête : juste cette succession étiquette-valeur, répétée.

L’étiquette

L’étiquette est un nombre qui contient, astucieusement compressées, deux informations :

  • le numéro du champ (champ n°1, n°2, n°3… — c’est protobuf qui numérote, il n’y a pas de noms à ce stade) ;
  • le type de forme de la valeur (appelé wire type, « type sur le fil »).

Il n’existe que quatre formes possibles :

FormeNomCe que c’est
0varintun nombre entier
164 bitsun nombre sur 8 octets (souvent un nombre à virgule)
2longueur + contenudu texte, ou une sous-liste de champs
532 bitsun nombre sur 4 octets

La forme n°2 est la plus riche : elle sert aussi bien pour du texte que pour ranger une structure à l’intérieur d’une autre. Protobuf est donc emboîtable : un champ peut contenir une sous-structure, qui contient elle-même des champs, et ainsi de suite. C’est pour cela que les données décodées ressemblent à des boîtes dans des boîtes.

Le « varint » : comment protobuf écrit les nombres

Protobuf range les nombres entiers d’une façon astucieuse appelée varint : un petit nombre prend peu de place, un grand nombre en prend plus. Pour cet article, retenez simplement que c’est une façon compacte d’écrire un entier ; les outils savent le relire automatiquement, on n’a pas besoin de le faire à la main.

Un détail qui aura son importance : le « zigzag »

Quand un nombre peut être négatif (comme une coordonnée dans l’hémisphère sud), protobuf utilise une variante d’encodage appelée zigzag. Sans entrer dans le calcul, l’idée est : le zigzag « replie » les nombres pour que les petits négatifs restent compacts.

La conséquence pratique : un nombre stocké dans le fichier n’est pas directement la valeur réelle. Il faut lui appliquer une petite transformation (le « décodage zigzag ») pour retrouver la vraie valeur, qui peut être négative. On verra que c’est exactement le piège des coordonnées GPS.

Ce qu’il faut retenir de l’étape 2

Un fichier protobuf = une suite de champs. Chaque champ a un numéro et une forme. Les champs peuvent s’emboîter. On peut lire la structure d’un fichier protobuf sans rien connaître d’autre — mais on n’aura que des numéros (« champ 3 »), pas encore de noms (« latitude »). Donner les noms, ce sera l’étape 6.


6. Étape 3 — Décoder le .nts (le résumé)

But de l’étape : lire le fichier résumé, plus simple, pour se faire la main.

Il existe un outil officiel pour lire un fichier protobuf : protoc, le « compilateur » de Protocol Buffers. Il possède une option, --decode_raw, qui affiche la structure d’un fichier protobuf sans avoir besoin du plan. C’est exactement ce qu’il nous faut pour commencer.

Le .nts est le plus simple des deux fichiers : il contient un seul bloc protobuf. On peut donc l’envoyer directement à protoc :

code
protoc --decode_raw < mon_fichier.nts

Note pour les utilisateurs de Windows / PowerShell. Le symbole < ci-dessus ne fonctionne pas dans PowerShell (il y est interdit). Il faut écrire à la place :

code
cmd /c "protoc --decode_raw < mon_fichier.nts"

Explication dans la section tutoriel.

Le résultat ressemble à ceci (extrait) :

code
1: 2
2: 0
3 {
  1 { 1: 17946197   2: 83170666 }
  2 { 1: 28279287   2: 81508812 }
}
4 { 1 { 1: 1763751347   2: 601 }   2 { 1: 21600   2: "Africa/Dar_es_Salaam" } }

On voit la structure : un champ 1, un champ 2, un champ 3 qui contient lui-même des sous-champs, etc. Les accolades { } marquent les structures emboîtées. Mais à ce stade, on ne sait pas encore ce que signifie « champ 3 ». On y reviendra.

On retiendra déjà que le .nts contient visiblement des coordonnées (les grands nombres du champ 3), une date (le grand nombre 1763751347 ressemble à un horodatage) et un nom de fuseau horaire.

Bon à savoir : le .nts est un résumé recalculable. Toutes ses valeurs (zone géographique, durée, distance…) peuvent être recalculées à partir du .ntf. C’est un index léger que l’application lit pour afficher rapidement un aperçu du tracé. Cela donne une vérification gratuite : si le résumé recalculé depuis le .ntf correspond au .nts fourni, le décodage est juste.


7. Étape 4 — Décoder le .ntf (le tracé)

But de l’étape : lire le fichier de tracé — plus gros, et organisé différemment.

Si on tente la même commande sur le .ntf :

code
protoc --decode_raw < mon_fichier.ntf

…on obtient une erreur : Failed to parse input (« impossible d’analyser »).

Pourquoi ? Parce que le .ntf n’est pas un seul bloc protobuf, mais une longue suite de blocs mis bout à bout. Et chaque bloc est précédé d’un petit nombre qui indique sa taille.

Une image. Le .ntf est comme un train de wagons accrochés les uns aux autres, sans séparation visible. Avant chaque wagon, une étiquette indique sa longueur. On ne peut pas lire le train d’un coup : il faut lire l’étiquette (« le prochain wagon fait 47 octets »), détacher ce wagon, l’ouvrir, puis passer au suivant. L’outil protoc, lui, s’attend à recevoir un seul wagon — d’où l’erreur.

La solution : un petit programme qui fait le travail de découpage. Il lit la taille, détache un bloc, recommence — et nous donne chaque bloc séparément. Ce programme est fourni en annexe (ntf_record.py). Une fois le découpage fait, chaque bloc isolé est, lui, un fichier protobuf normal que protoc sait lire :

code
# combien de blocs dans le fichier ?
python3 ntf_record.py mon_fichier.ntf --count

# afficher la structure du bloc n°1
python3 ntf_record.py mon_fichier.ntf 1 | protoc --decode_raw

En examinant ces blocs, on découvre que le .ntf contient des dizaines de milliers de blocs : ce sont les points du tracé, un bloc par point.

Conclusion de l’étape 4 : le .ntf est un flux de blocs ; chaque bloc est un point du tracé ; il faut d’abord découper, ensuite décoder.


8. Le découpage en détail — sur les vrais octets

Le découpage est la mécanique de base : sans elle, rien n’est possible. Elle paraît mystérieuse, mais elle est en réalité très simple — c’est de la lecture, pas du calcul. Détaillons-la sur les vrais octets d’un fichier.

L’image du colis

Imaginez des colis sur un tapis roulant, collés les uns aux autres, sans séparateur. Sur chaque colis, une étiquette à l’avant indique sa taille : « ce colis fait 49 cm ». Vous lisez l’étiquette, vous mesurez 49 cm, vous coupez : le premier colis est isolé. Vous êtes alors pile devant l’étiquette du suivant. Vous recommencez. Vous n’avez jamais à deviner où couper : l’étiquette vous le dit à chaque fois.

Le .ntf est exactement cela :

code
[longueur][..... bloc 1 .....][longueur][..... bloc 2 .....][longueur][... bloc 3 ...]

La procédure

On utilise un curseur : un repère qui pointe un endroit dans le fichier. Au départ, il est à la position 0 (tout début).

  1. À l’endroit du curseur, lire le nombre « longueur ».
  2. Avancer le curseur juste après ce nombre.
  3. Prélever autant d’octets que la longueur indiquée : c’est un bloc.
  4. Avancer le curseur après le bloc.
  5. Recommencer au point 1, tant qu’on n’a pas atteint la fin du fichier.

Aucun calcul savant : on lit une longueur, on prélève, on avance, on répète.

Sur les vrais octets du fichier

Les 40 premiers octets d’un .ntf réel, en hexadécimal :

code
31 08 01 12 29 0a 09 08 b3 eb 82 c9 06 10 d9 04 12 1c 08 e0 a8 01 ...
└┘ └────────────────────── ... ──────────────────────────────┘
↑          ↑
longueur   contenu du bloc 1

Bloc 1. Le curseur est à la position 0. L’octet qui s’y trouve est 0x31. En décimal, 0x31 vaut 49. C’est la longueur : le bloc 1 fait 49 octets. Le curseur avance d’un cran (position 1), on prélève 49 octets — c’est le bloc 1 — puis le curseur avance de 49 : il est à la position 50.

Bloc 2. Le curseur est à la position 50. L’octet est 0x35, soit 53. Curseur +1 (position 51), on prélève 53 octets, curseur +53 : position 104.

Bloc 3. Le curseur est à la position 104. L’octet est 0x26, soit 38. Curseur +1, on prélève 38 octets, curseur +38 : position 143.

Le tableau résume la mécanique :

Bloc 1Bloc 2Bloc 3
curseur au départ050104
octet de longueur lu0x31 = 490x35 = 530x26 = 38
curseur après le bloc50104143

Le « curseur après le bloc » d’une ligne devient le « curseur au départ » de la suivante : la chaîne s’enchaîne d’elle-même, et continue à l’identique jusqu’à la fin du fichier.

Le mot juste : la longueur n’est pas calculée, elle est lue. Elle est déjà écrite dans le fichier, juste devant chaque bloc — l’application l’y a mise exprès pour qu’on puisse découper. Le terme technique pour ce genre de format est « préfixé par la longueur » (length-prefixed).

Quand la longueur tient sur deux octets

Dans l’exemple ci-dessus, les longueurs (49, 53, 38) tenaient chacune sur un seul octet, parce qu’elles sont petites. Mais une longueur peut dépasser ce qu’un octet peut contenir (un octet ne va que jusqu’à 255). La longueur s’écrit alors sur plusieurs octets, selon la règle du varint.

Cette règle repose sur un seul bit de chaque octet — le bit le plus à gauche, qui sert de feu de signalisation :

  • bit de gauche = 1 → « ça continue, l’octet suivant fait encore partie du nombre » ;
  • bit de gauche = 0 → « c’est fini, dernier octet du nombre ».

Le truc pratique pour le repérer sans convertir en binaire : il suffit de regarder la valeur de l’octet.

  • octet inférieur à 0x80 (soit 00 à 7f) → bit de gauche à 0 → dernier octet ;
  • octet égal ou supérieur à 0x80 (soit 80 à ff) → bit de gauche à 1 → ça continue.

Règle de poche : un octet ≥ 0x80, le nombre continue ; un octet < 0x80, le nombre s’arrête là.

Exemples concrets :

LongueurNombre d’octetsEn hexadécimalEn binaire
4913100110001
12717f01111111
128280 0110000000 00000001
3002ac 0210101100 00000010

Pour la longueur 300 → ac 02 :

code
1er octet : ac = 1010 1100   bit de gauche = 1  → "ça continue"
2e octet  : 02 = 0000 0010   bit de gauche = 0  → "c'est fini"

Le premier octet 0xac (= 172) est ≥ 0x80 : signal que le nombre n’est pas terminé. On lit donc le deuxième octet, 0x02, qui est < 0x80 : fin du nombre. La longueur occupait 2 octets.

C’est exactement ce que fait la fonction read_varint du script (annexe A) : elle lit les octets un par un et s’arrête au premier octet inférieur à 0x80. C’est aussi pourquoi elle renvoie la nouvelle position du curseur : elle seule sait si la longueur a occupé un, deux ou trois octets, et fait donc avancer le curseur du bon nombre de crans.

Le code, relu avec tout ça en tête

Le cœur de ntf_record.py se lit maintenant comme du français :

code
def records(buf):                     # buf = tout le contenu du fichier
    pos = 0                            # le curseur, au tout début
    while pos < len(buf):              # tant qu'on n'est pas au bout :
        ln, pos = read_varint(buf, pos)   # 1+2 : lire la longueur, avancer le curseur
        yield buf[pos:pos+ln]             # 3   : prélever 'ln' octets = un bloc
        pos += ln                         # 4   : avancer le curseur après le bloc
                                          # 5   : la boucle 'while' recommence

Quatre lignes. ln est la longueur lue ; buf[pos:pos+ln] désigne les octets du curseur jusqu’à curseur + longueur (le prélèvement du colis) ; pos += ln fait avancer le curseur. La boucle reprend toute seule jusqu’à la fin du fichier.


9. Étape 5 — Donner un sens aux données

But de l’étape : passer des numéros (« champ 3 ») au sens (« latitude »).

À ce stade, on sait lire la structure mais pas interpréter. protoc nous donne par exemple, pour un point :

code
1: 0
2 { 1 { 1: 2   2: 228 } }
3 { 1 { 1: 45   2: 0 } }
5 { 1: 12   2: 199 }

Des numéros et des nombres. Pour transformer ça en information utile, quatre sous-étapes, puis une vérification.

9.1 Deviner le rôle de chaque champ

C’est un travail de détective. On compare beaucoup de points entre eux et on raisonne sur les valeurs :

  • un champ qui ne vaut jamais que 0 ou 1 → c’est probablement un interrupteur (oui/non) ;
  • un champ qui vaut environ 1,7 milliard → c’est un horodatage (le nombre de secondes écoulées depuis 1970, une convention informatique universelle) ;
  • un champ qui augmente régulièrement de point en point → un compteur ;
  • un champ entre 0 et 36000 → probablement un cap (une direction en degrés, multipliée par 100).

On recoupe aussi avec ce qu’on connaît : le .nts (qui contient les mêmes types de valeurs), et la réalité (on convertit des coordonnées candidates et on vérifie qu’elles tombent bien en mer, dans une zone cohérente).

9.2 Appliquer les bonnes transformations

Les nombres bruts doivent souvent être retravaillés :

  • les nombres qui peuvent être négatifs sont en zigzag : il faut appliquer le décodage zigzag pour retrouver la vraie valeur (voir étape 2) ;
  • les coordonnées ne sont pas en degrés directement : elles sont multipliées par un million. Le nombre 8 973 097 stocké dans le fichier signifie en réalité 8,973097 degrés. Il faut donc diviser par 1 000 000 ;
  • les horodatages (secondes depuis 1970) se convertissent en date et heure normales.

9.3 Recoller les points entre eux — le point délicat

C’est ici qu’intervient la nuance la plus importante de tout l’article.

Quand on regarde les points du .ntf, on s’aperçoit que le premier point contient des valeurs complètes (une vraie date, une vraie position), mais que les points suivants ne contiennent que de tout petits nombres.

Cela veut dire que les points suivants ne stockent pas leur position complète, mais seulement un écart. C’est une technique de compression classique : plutôt que de réécrire à chaque fois « 8,973097° de latitude », on écrit juste « +0,000023° par rapport à la référence », ce qui prend bien moins de place.

Le premier point sert donc de point de référence (on l’appellera l’ancre). Mais il y a alors deux façons possibles de comprendre les écarts des points suivants — et c’est là qu’on peut se tromper :

Façon A — « par rapport au point précédent ». Chaque écart se compte depuis le point d’avant. « Le bateau a avancé de 3 mètres depuis la note précédente. » Pour connaître la position réelle d’un point, il faut additionner tous les écarts depuis le début, dans l’ordre.

Façon B — « par rapport à l’ancre ». Chaque écart se compte depuis le point de référence fixe (le premier point). « Le bateau est à 41 mètres du point de départ. » Chaque point se lit tout seul : il suffit d’ajouter son écart à l’ancre.

Ces deux façons sont visuellement identiques dans le fichier — les mêmes petits nombres. Rien dans le format ne dit laquelle est la bonne. Il faut le découvrir en essayant.

Et c’est exactement l’erreur qui a été commise en cours de route : on a d’abord supposé la façon A (additionner depuis le point précédent). Résultat : des positions absurdes — une latitude de 159 000 degrés, une date en l’an 2243. En testant l’autre hypothèse, la façon B (chaque écart compté depuis l’ancre), tout est rentré dans l’ordre instantanément.

La bonne réponse pour Navionics est donc la façon B : chaque point est un écart direct par rapport à l’ancre, et non par rapport au point précédent. Autrement dit, une fois l’ancre connue, chaque point se reconstitue indépendamment des autres : on prend l’ancre, on ajoute l’écart du point, on a sa position absolue.

C’est précisément cette règle-là — « écart direct, pas cumulé » — qu’aucun plan, aucun schéma ne pouvait nous donner : elle ne se trouve qu’en testant contre un vrai fichier et en regardant si le résultat a un sens.

9.4 Mettre en forme

Une fois chaque point transformé en (date, latitude, longitude, vitesse), on dispose d’une liste de positions. On l’enregistre dans un format standard, le GPX — un format de tracé que lisent tous les logiciels de cartographie. On obtient enfin un fichier ouvrable dans Google Earth, un SIG, ou un logiciel d’analyse.

9.5 Vérifier

Dernière sous-étape, essentielle : on recalcule, à partir du tracé reconstitué, des grandeurs globales (la zone géographique couverte, la durée totale) et on les compare au contenu du .nts (le résumé). Si les deux concordent, c’est que l’interprétation est juste. C’est ce qu’on appelle une validation croisée : deux sources indépendantes qui disent la même chose.


10. Étape 6 — Retrouver le « plan » officiel du format

But de l’étape : au lieu de deviner le rôle des champs, retrouver le plan exact que les développeurs de Navionics ont utilisé.

Jusqu’ici, on a deviné. C’est efficace, mais on aimerait une certitude, et les vrais noms des champs. Le plan (.proto) n’est pas livré avec l’application — mais il est peut-être récupérable. Voici la chasse, étape par étape. C’est la partie la plus « enquête » de l’article.

10.1 Première piste : chercher des fichiers .proto dans l’application

On peut récupérer l’application elle-même (sous Android, c’est un fichier .apk ; on peut l’obtenir légalement, par exemple sur un site de téléchargement d’APK, ou depuis un téléphone) et regarder à l’intérieur — un .apk est en réalité une simple archive compressée.

En cherchant, on trouve effectivement des fichiers .proto. Déception : en les ouvrant, on constate qu’ils portent tous la mention package google.protobuf. Ce sont les fichiers standards de protobuf lui-même, livrés avec n’importe quelle application qui utilise cette technologie. C’est l’équivalent de trouver des pièces standard dans une voiture : ça confirme la marque du moteur, mais ça n’apprend rien sur ce véhicule en particulier.

Le critère simple pour trier : un .proto marqué package google.protobuf = standard, sans intérêt. Un .proto avec un autre nom = potentiellement le plan recherché. Ici, on n’en trouve aucun. Première piste : impasse.

10.2 Deuxième piste : regarder dans le code de l’application

L’application contient son code, dans des fichiers appelés .dex. On peut les inspecter. On y trouve bien des éléments liés aux tracés — mais aucun ne contient le plan du format. En revanche, on remarque un détail décisif : le code mentionne une partie « native ».

Explication. Une application est souvent composée de deux morceaux : une partie écrite dans le langage habituel du téléphone, et une partie « native », écrite dans un langage plus proche de la machine (C++), réunie dans des fichiers .so. Ici, le code montre que toute la gestion des tracés se passe dans la partie native. Le plan du format n’est donc pas dans le code inspecté : il est dans un fichier .so.

Cette piste-là n’a pas donné le plan, mais elle a fait quelque chose d’important : elle a indiqué où chercher.

10.3 Troisième piste : la bibliothèque native

On récupère donc le bon fichier .so — la bibliothèque native principale de Navionics (un fichier d’une trentaine de mégaoctets). C’est un programme compilé : du code transformé en instructions machine, pas du tout lisible tel quel.

Mais un programme compilé conserve presque toujours une liste de noms internes : les noms des « structures » et des « fonctions » que les développeurs ont utilisés. Ces noms sont nécessaires au bon fonctionnement du programme, et ils survivent dans le fichier. On peut les extraire.

Et là, on touche le but : parmi ces noms, on trouve toute une famille de structures clairement liées au format du tracé — NavPoint, NavLatLon, NavLocation, NavTime, NavVelocity, NavMeta, et d’autres. Ce sont les structures du format Navionics. NavPoint (« point de navigation »), avec son suffixe au singulier, est manifestement la structure d’un point du tracé.

10.4 Lire le plan dans le programme

Reste à connaître, pour chaque structure, la liste de ses champs. Le programme contient une fonction qui écrit chaque structure dans le fichier. En examinant cette fonction — on « désassemble » le programme, c’est-à-dire qu’on traduit le code machine en instructions lisibles — on voit défiler, dans l’ordre, l’écriture de chaque champ : son numéro et sa forme.

De plus, le programme contient aussi les noms des champs, rangés à part. En croisant les deux — la liste des numéros/formes d’un côté, la liste des noms de l’autre — on reconstitue le plan complet.

Et ce plan est remarquablement clair, car les développeurs de Navionics ont donné à leurs champs des noms explicites, avec les unités :

  • LatitudeDegX1000000 → « latitude en degrés, multipliée par 1 000 000 » ;
  • SpeedMsX100 → « vitesse en mètres par seconde, multipliée par 100 » ;
  • UTCUnixTimeS → « date, en secondes depuis 1970 » ;
  • Millisecond → la fraction de seconde ;
  • et ainsi de suite.

Le nom du champ nous donne gratuitement l’unité et le facteur de conversion.

10.5 Le plan reconstitué

On obtient ainsi le plan complet du format — 22 structures. Les deux plus importantes :

Un point de tracé (NavPoint) contient : un interrupteur « point complet ou simple écart », un sous-bloc temps, un sous-bloc position, et des sous-blocs optionnels profondeur, vitesse, température de l’eau, événement, poisson.

Une position (NavLocation → NavLatLon) contient : la latitude et la longitude (en millionièmes de degré), l’altitude, et la précision de la mesure.

Le plan complet est fourni en annexe C.

10.6 Ce que le plan a confirmé

Le plan officiel a validé toutes les hypothèses faites à l’étape 5 en devinant :

On avait deviné…Le plan confirme…
un interrupteur « complet / écart »le champ IsFull (oui/non)
un champ « position »le champ Location
coordonnées multipliées par un millionle nom dit …DegX1000000
les coordonnées peuvent être négatives (zigzag)le type confirme le zigzag

On est passé de « interprétation très probable » à « structure confirmée par le code de l’application ». Pour un dossier sérieux, c’est la différence entre une supposition et une preuve.

10.7 Ce que le plan ne dit pas

Point crucial, déjà annoncé : le plan décrit la forme des données (« le champ 3 est une position »). Il ne dit pas la règle d’interprétation des écarts — la fameuse « façon A ou façon B » de l’étape 5.3. Cette règle n’est ni dans le plan, ni dans la fonction d’écriture : elle se vérifie en testant. Le plan et le test sont donc complémentaires : il faut les deux.


11. Étape 7 — Écrire le décodeur final

But de l’étape : réunir tout ce qui précède dans un programme qui décode le .ntf automatiquement.

On dispose maintenant de tout :

  • les règles de protobuf (étape 2) ;
  • le découpage du flux en blocs (étape 4) ;
  • le plan du format, avec noms et unités (étape 6) ;
  • la règle d’interprétation : chaque point est un écart direct par rapport à l’ancre (étape 5.3).

Le décodeur final (fourni en annexe, navionics_ntf_parser.py) enchaîne : découper le flux → décoder chaque bloc selon le plan → appliquer les transformations (zigzag, division par un million, conversion des dates) → recoller chaque point à l’ancre → écrire le tout en GPX.

Le résultat, sur le fichier analysé :

  • 37 456 points GPS, chacun horodaté ;
  • une période de navigation de plus de 61 heures ;
  • un fichier GPX prêt à être ouvert dans n’importe quel logiciel de cartographie.

La vérification finale. Ce résultat est identique, au chiffre près, à celui obtenu tout au début par simple devinette (étape 5) : même nombre de points, mêmes dates, même zone géographique. Deux chemins indépendants — la devinette du début et le plan extrait du programme — aboutissent exactement au même tracé. C’est la preuve que le décodage est correct.


12. La leçon : ce que chaque méthode apporte

Cet article a suivi un parcours en zigzag, avec des impasses. Ce n’est pas un défaut du récit : c’est ainsi que fonctionne réellement une rétro-ingénierie. Chaque méthode a apporté une pièce :

  • Deviner (étape 5) a donné la structure générale et la règle des écarts — mais avec des noms de champs inventés.
  • Chercher des .proto tout faits (étape 6.1) n’a rien donné — mais a écarté une hypothèse.
  • Inspecter le code de l’application (étape 6.2) n’a pas donné le plan non plus — mais a révélé où il se trouvait (la partie native).
  • Analyser la bibliothèque native (étapes 6.3 à 6.5) a donné le plan complet, avec les vrais noms et les unités — mais pas la règle des écarts.

Aucune méthode seule ne suffisait. La devinette de départ et le plan officiel de la fin sont complémentaires : l’un a donné la logique, l’autre les noms ; et c’est leur réunion, vérifiée contre le fichier réel, qui produit un décodeur correct.

Pour quiconque débute : retenez surtout cette idée. On avance par hypothèses, on accepte les impasses (elles vous disent où chercher ensuite), et on vérifie toujours le résultat contre quelque chose d’indépendant.


13. Tutoriel reproductible — toutes les commandes

Cette section regroupe toutes les commandes pour refaire la manipulation de bout en bout et obtenir le tracé GPX. Les scripts mentionnés sont en annexe.

13.1 Outils à installer

OutilÀ quoi il sertInstallation
Python 3exécuter les scripts de décodageinclus sur Mac/Linux ; sur Windows : winget install Python.Python.3.12
protocafficher la structure d’un fichier protobufvoir ci-dessous
binutils aarch64examiner la bibliothèque native (étape 6)sudo apt install binutils-aarch64-linux-gnu (Linux)
un dézippeurouvrir l’.apk et la bibliothèque (étape 6)unzip, inclus partout

Les deux derniers ne servent que si vous voulez refaire vous-même l’extraction du plan (étape 6). Pour seulement décoder un .ntf, Python et les scripts fournis suffisent.

Installer protoc :

  • Windows : winget install protobuf puis ouvrir un nouveau terminal.
  • Mac : brew install protobuf.
  • Linux : sudo apt install protobuf-compiler.

Vérifier que ça marche : protoc --version doit afficher un numéro.

13.2 Décoder le résumé .nts

code
# Linux / Mac :
protoc --decode_raw < fichier.nts
code
# Windows (PowerShell) :
cmd /c "protoc --decode_raw < fichier.nts"

Pourquoi le cmd /c sous Windows ? PowerShell interdit le symbole <. La commande cmd /c "..." confie l’opération à l’ancien terminal Windows, qui, lui, l’accepte. À défaut, on peut faire toute la manipulation sous Linux, ou dans le sous-système Linux de Windows (WSL).

13.3 Explorer le tracé .ntf

Le .ntf ne se décode pas directement (c’est un flux de blocs). On utilise le script ntf_record.py (annexe A) pour le découper :

code
# nombre de blocs (points) dans le fichier
python3 ntf_record.py fichier.ntf --count

# afficher la structure du bloc n°1
python3 ntf_record.py fichier.ntf 1 | protoc --decode_raw

Sous Windows, encadrer la deuxième commande dans cmd /c "..." pour la même raison que ci-dessus.

13.4 Décoder le tracé complet en GPX

C’est l’étape finale. Le script navionics_ntf_parser.py (annexe B) fait tout automatiquement :

code
python3 navionics_ntf_parser.py fichier.ntf tracé.gpx

Le script affiche un résumé (nombre de points, période, zone, vitesses) et écrit le fichier tracé.gpx. Ce GPX s’ouvre dans Google Earth, QGIS, ou tout logiciel de cartographie.

13.5 (Optionnel) Refaire l’extraction du plan — étape 6

Réservé à ceux qui veulent reproduire la rétro-ingénierie complète.

code
# 1. ouvrir l'apk de l'application (c'est une archive)
unzip base.apk -d apk_extrait

# 2. la bibliothèque native est dans un "split" de l'apk
unzip split_config.arm64_v8a.apk lib/arm64-v8a/libnavnative.so

# 3. extraire la liste des noms internes liés au format
#    (les structures du tracé apparaissent sous la forme Nav...)
aarch64-linux-gnu-readelf -W --dyn-syms libnavnative.so | grep "Nav"

# 4. extraire les noms de champs de chaque structure
aarch64-linux-gnu-readelf -W --dyn-syms libnavnative.so | grep "FieldNumber"

La lecture précise du plan (numéros + formes des champs) demande de désassembler la bibliothèque ; cette partie, plus avancée, est décrite à l’étape 10.4 et utilise l’outil objdump (paquet binutils-aarch64-linux-gnu) ou un logiciel d’analyse comme Ghidra (gratuit, édité par la NSA). Le plan déjà reconstitué étant fourni en annexe C, il n’est pas nécessaire de refaire cette extraction pour décoder un .ntf.

13.6 Récapitulatif du parcours

code
fichier .ntf (illisible)
   │
   │  étape 1 : file + hexdump        -> c'est du protobuf, non chiffré
   │  étape 4 : ntf_record.py         -> découpage en blocs (les points)
   │  étape 3 : protoc --decode_raw   -> structure visible (numéros)
   │  étape 6 : plan extrait du .so   -> noms et unités des champs
   │  étape 5 : règle des écarts      -> chaque point = écart depuis l'ancre
   │  étape 7 : navionics_ntf_parser  -> décodage complet
   ▼
tracé.gpx (lisible dans tout logiciel de cartographie)

14. Annexes

Annexe A — ntf_record.py (découpe le flux .ntf en blocs)

code
#!/usr/bin/env python3
# Découpe un fichier .ntf en ses blocs successifs.
# Le .ntf est une suite de [taille][bloc][taille][bloc]...
#
# Usage :
#   python3 ntf_record.py fichier.ntf --count   -> nombre de blocs
#   python3 ntf_record.py fichier.ntf N         -> envoie le bloc N
#   python3 ntf_record.py fichier.ntf N | protoc --decode_raw
import sys

def read_varint(buf, pos):
    result = shift = 0
    while True:
        b = buf[pos]; pos += 1
        result |= (b & 0x7f) << shift
        if not (b & 0x80):
            return result, pos
        shift += 7

def records(buf):
    pos = 0
    while pos < len(buf):
        ln, pos = read_varint(buf, pos)
        yield buf[pos:pos+ln]
        pos += ln

if __name__ == "__main__":
    data = open(sys.argv[1], "rb").read()
    if sys.argv[2] == "--count":
        print(sum(1 for _ in records(data)))
    else:
        n = int(sys.argv[2])
        for i, rec in enumerate(records(data)):
            if i == n:
                sys.stdout.buffer.write(rec)
                break

Annexe B — navionics_ntf_parser.py (décodeur complet .ntf → GPX)

Le décodeur complet est fourni dans le fichier séparé navionics_ntf_parser.py. Il est piloté par le plan de l’annexe C et applique la règle « chaque point est un écart direct par rapport à l’ancre ».

Utilisation :

code
python3 navionics_ntf_parser.py fichier.ntf tracé.gpx

Annexe C — Le plan du format (navionics_track.proto)

Le plan complet reconstitué — les 22 structures, avec numéros, formes et noms de champs — est fourni dans le fichier séparé navionics_track.proto.

Rappel : il s’agit d’une reconstitution par rétro-ingénierie, pas d’une spécification officielle de Navionics. Les structures principales sont NavPoint (un point du tracé), NavLocation et NavLatLon (la position), NavTime (l’horodatage) et NavMeta (le résumé contenu dans le .nts).

Annexe D — Glossaire

TermeExplication simple
Binairefichier de données brutes, non destiné à l’œil humain (≠ chiffré)
Hexadécimalmanière compacte d’écrire des nombres ; sert à afficher les octets
Octetla plus petite unité d’un fichier (un nombre de 0 à 255)
Protobufméthode de Google pour ranger des données dans un fichier binaire
Schéma / .protole « plan » qui décrit ce que contient un fichier protobuf
Champun renseignement dans un fichier protobuf (a un numéro et une forme)
Varintfaçon compacte d’écrire un nombre entier en protobuf
Zigzagvariante d’encodage pour les nombres pouvant être négatifs
Ancrele point de référence (le premier point complet du tracé)
Sérialisérangé sous forme de fichier selon des règles précises
Rétro-ingénieriecomprendre un système sans sa documentation, en l’observant
.so / bibliothèque nativepartie d’une application écrite en langage proche de la machine
Désassemblertraduire un programme compilé en instructions lisibles
GPXformat de fichier standard pour les tracés GPS

Annexe E — Aide-mémoire des commandes

code
# Identifier le fichier
file fichier.ntf

# Voir le contenu brut (hexdump)
od -A x -t x1z fichier.ntf | head

# Décoder le résumé .nts
protoc --decode_raw < fichier.nts                  # Linux / Mac
cmd /c "protoc --decode_raw < fichier.nts"         # Windows PowerShell

# Explorer le tracé .ntf
python3 ntf_record.py fichier.ntf --count
python3 ntf_record.py fichier.ntf 1 | protoc --decode_raw

# Décoder le tracé complet en GPX
python3 navionics_ntf_parser.py fichier.ntf tracé.gpx
FR EN