Salsa et utopia


L’application Utopia

Utopia est une application offrant une fonctionnalité de messagerie instantanée et d’appels vocaux, mais proposant aussi des boucles comparables aux à celles de telegram, ainsi que des services en lien avec les cryptomonnaies.

C’est à la fonctionnalité de messagerie instantanée qu’on s’intéresse.

Un passage sur le site internet nous apprends que l’échange de messages s’effectue au dessus d’un réseau P2P.

Les motivations, l’identité et la nature juridique du développeur restent floues.

L’interface de l’application propose un onglet listant les conversations :

un autre onglet listant les boucles :

et un dernier onglet lié aux fonctionnalités crypto :

Le trafic de l’application

On intercepte le trafic de l’application avec tcpdump et on examine le pcap obtenu avec wireshark :

Le trafic ne correspond à rien de connu, en tout cas à rien de connu de wireshark !

À la recherche du chiffrement

Une question naturelle est de s’intéresser au mécanisme de chiffrement des messages. Comme il s’agit d’une application Android, le chiffrement peut être réalisé dans le code java/kotlin dans l’application (comme c’était le cas pour Seatalk), ou dans une bibliothèque native propre à l’application.

Comme c’est le chemin le plus aisé, on commence par regarder les parties non-natives de l’application.

Avec frida, et plus exactement avec frida-trace, on trace les appels à divers packages et fonctions qui pourraient être des bons candidats pour chiffrer les messages textes :

  • les packages et fonctions usuels du JRE impliqués dans du chiffrement : javax.crypto.*, update, updateAAD, getIV, doFinal, Cipher, etc
  • d’éventuelles fonctions dont le nom mentionne un algorithme de chiffrement usuel : AES, SHA256, SHA512, etc
  • les packages dont le nom contient la chaîne de caractères « message » (comme is.u.utopia.message)

Malheureusement, tout ceci ne mène à rien. Une idée naturelle est de regarder les bibliothèques natives embarquées dans l’application, qui en contient un nombre très important (118 !).

En les triant par taille, la plus grande est libUtopia_arm64-v8a.so :

$ ls -lhSr utopia_app/is.u.utopia-gvdTWTlayzyirVLz5riYkQ\=\=/lib/arm64/
total 303M
(...)
-rw-r--r-- 1 thomas thomas 5,4M 14 janv. 21:06 libQt6Qml_arm64-v8a.so
-rw-r--r-- 1 thomas thomas 5,9M 14 janv. 21:06 libQt6Core_arm64-v8a.so
-rw-r--r-- 1 thomas thomas 7,0M 14 janv. 21:06 libQt6Quick_arm64-v8a.so
-rw-r--r-- 1 thomas thomas 7,6M 14 janv. 21:06 libQt6Gui_arm64-v8a.so
-rw-r--r-- 1 thomas thomas  13M 14 janv. 21:06 libavcodec.so
-rw-r--r-- 1 thomas thomas  42M 14 janv. 21:06 libvlc.so
-rw-r--r-- 1 thomas thomas 163M 14 janv. 21:06 libUtopia_arm64-v8a.so

Intéressons-nous à cette bibliothèque, et traçons certaines de ses fonctions. On suit la même démarche que celle utilisée sans succès dans le code non-natif.

  • On trace tout ce qui est en rapport avec aes/Aes/AES : cela ne donne rien
  • On frida-trace les fonctions dont le nom contient « salsa » : Bingo !

Certaine fonctions sont appelées très régulièrement :

crypto_stream_salsa20_xor_ic, crypto_stream_salsa20_xor, crypto_secretbox_xsalsa20poly1305_open, crypto_secretbox_xsalsa20poly1305, crypto_box_curve25519xsalsa20poly1305_afternm.

Une petite recherche sur un moteur de recherche nous informe que ces fonctions proviennent de la bibliothèque cryptographique libsodium.

Les sources de cette bibliothèque nous éclairent sur les relations qu’entretiennent entre elles ces fonctions, informations que nous pouvons corroborer dynamiquement :

Pour cela, on crée des scripts frida hookant ces fonctions, et l’on ajoute un appel à Thread.backtrace(this.context, Backtracer.ACCURATE) pour dumper la pile d’appel dans les hooks de ces fonctions.

Il ressort de tout cela que :

  • crypto_stream_salsa20_xor appelle crypto_stream_salsa20_xor_ic,
  • crypto_secretbox_xsalsa20poly1305_open appelle crypto_stream_salsa20_xor,
  • crypto_box_curve25519xsalsa20poly1305_afternm appelle crypto_secretbox_xsalsa20poly1305,
  • crypto_secretbox_xsalsa20poly1305 appelle crypto_stream_salsa20_xor.

Compte tenu de tout ceci, compte tenu également des fréquences auxquelles ces différentes fonctions sont appelées, il semble pertinent de se concentrer sur les appels à crypto_secretbox_xsalsa20poly1305.

Chiffrement avec XSalsa20

utopia::crypto::encrypt(std::__ndk1::span<unsigned char const, 18446744073709551615ul>,
                        std::__ndk1::span<unsigned char const, 18446744073709551615ul>,
                        std::__ndk1::span<unsigned char const, 18446744073709551615ul>,
                        utopia::utils::ByteArray&,
                        utopia::utils::ByteArray&,
                        utopia::utils::ByteArray&)

et

_ZN6utopia6crypto7encryptENSt6__ndk14spanIKhLm18446744073709551615EEES4_S4_RNS_5utils9ByteArrayE

dont la signature est :

utopia::crypto::encrypt(std::__ndk1::span<unsigned char const, 18446744073709551615ul>,
                        std::__ndk1::span<unsigned char const, 18446744073709551615ul>,
                        std::__ndk1::span<unsigned char const, 18446744073709551615ul>,
                        utopia::utils::ByteArray&)

ces deux fonctions (que nous nommerons encrypt(1, 2, 3, 4, 5, 6) et encrypt(1, 2, 3, 4)) sont appelées par utopia::PrivateMessage::sendInstantMessage(utopia::AddressInfo const&, utopia::PrivateMessageInfo const&)

Un petit coup d’oeil au code source de crypto_secretbox_xsalsa20poly1305 dans libsodium permet de comprendre les rôles des différents arguments :

int crypto_secretbox_xsalsa20poly1305(unsigned char *c,
                                      const unsigned char *m,
                                      unsigned long long mlen,
                                      const unsigned char *n,
                                      const unsigned char *k)

Si l’on dumpe le message (argument m) :

crypto_secretbox_xsalsa20poly1305() (thread n°25266) input:
ciphertext buffer addr:0x78792639b0
plaintext buffer content (addr = 0x7879506640):
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000020  73 61 6e 64 77 69 63 68 20 61 75 20 6a 61 6d 62  sandwich au jamb
00000030  6f 6e                                            on

Sa longueur (argument mlen) :

plaintext buffer length: 0x32

Le nonce (argument n) :

nonce:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  1f bf cd 18 44 ff f1 7f 03 ba cf 24 e6 0a 7e d3  ....D......$..~.
00000010  ba 62 c9 8d 95 c1 7c c5                          .b....|.

Et la clé (argument k) :

key (addr = 0x783d892680 ):
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  4d b8 0f 3e a4 74 12 eb ca 43 62 f9 e2 19 cd 4e  M..>.t...Cb....N
00000010  aa 8c 0b 10 5b 0d 76 27 16 ba 4c b4 64 56 71 34  ....[.v'..L.dVq4

en début d’exécution, puis le chiffré (argument c) en sortie :

ciphertext addr = 0x78792639b0
ciphertext content:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000010  9b a1 47 45 60 ec 93 af de c5 7b 27 08 6b 2e a4  ..GE`.....{'.k..
00000020  37 72 6a 0b 37 d8 70 df 54 be d9 15 bc a2 4d 4b  7rj.7.p.T.....MK
00000030  69 be 00 00 00 00 00 00 65 00 00 00 00 00 00 00  i.......e.......
00000040  04 01 04 00 00 00 e3 aa 00 00 00 00 00 00 00 00  ................
00000050  00 00 00 00 00 00 00 00 00 00 87 44 00 00 00 00  ...........D....
00000060  00 00 87 44 00 00 16 45 00 00 00 00 00 00 16 45  ...D...E.......E
00000070  e0 59                                            .Y

On peut reproduire le calcul avec un petit programme faisant appel à libsodium :

$ cat test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sodium.h>

unsigned char m[50] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                       0x73, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x63, 0x68,
                       0x20, 0x61, 0x75, 0x20, 0x6a, 0x61, 0x6d, 0x62,
                       0x6f, 0x6e};

unsigned char n[24] = {0x1f, 0xbf, 0xcd, 0x18, 0x44, 0xff, 0xf1, 0x7f,
                       0x03, 0xba, 0xcf, 0x24, 0xe6, 0x0a, 0x7e, 0xd3,
                       0xba, 0x62, 0xc9, 0x8d, 0x95, 0xc1, 0x7c, 0xc5};

unsigned char k[32] = {0x4d, 0xb8, 0x0f, 0x3e, 0xa4, 0x74, 0x12, 0xeb,
                       0xca, 0x43, 0x62, 0xf9, 0xe2, 0x19, 0xcd, 0x4e,
                       0xaa, 0x8c, 0x0b, 0x10, 0x5b, 0x0d, 0x76, 0x27,
                       0x16, 0xba, 0x4c, 0xb4, 0x64, 0x56, 0x71, 0x34};

int main()
{
    unsigned char *c = malloc(256 * sizeof(unsigned char));
    memset(c, 0x00, 256);

    //crypto_stream_xsalsa20_xor(c, m, 36, n, k);
    crypto_secretbox_xsalsa20poly1305(c, m, 50, n, k);

    for (int i = 0; i < 64; i++)
    {
        printf("%02X ", c[i]);
        if (i > 0 & i % 16 == 15)
        {
            printf("\n");
        }
    }

    return 0;
}
$ gcc test.c -o test -lsodium
$ ./test 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
9B A1 47 45 60 EC 93 AF DE C5 7B 27 08 6B 2E A4 
37 72 6A 0B 37 D8 70 DF 54 BE D9 15 BC A2 4D 4B 
69 BE 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Si l’on suit l’origine du message clair, on l’aperçoit auparavant dans la fonction appelante (la variante à 4 arguments de utopia::crypto::encrypt()), dans le premier argument :

utopia::crypto::encrypt(1, 2, 3, 4)
param1 addr: 0x783d896340
param1 content:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  73 61 6e 64 77 69 63 68 20 61 75 20 6a 61 6d 62  sandwich au jamb
00000010  6f 6e 60 c9 77 00 00 00 00 00 00 00 00 00 00 00  on`.w...........
00000020  02 01 02 00 00 00 57 2f 00 00 00 00 00 00 00 00  ......W/........
00000030  02 00 00 00 00 00 00 00 90 07 aa da 77 00 00 00  ............w...
00000040  a0 07 aa da 77 00 00 00 a0 07 aa da 77 00 00 00  ....w.......w...
00000050  02 01 02 00 00 00 63 1f 00 00 00 00 00 00 00 00  ......c.........
00000060  02 00 00 00 00 00 00 00 b0 fc a8 da 77 00 00 00  ............w...
00000070  c0 fc a8 da 77 00 00 00 c0 fc a8 da 77 00 00 00  ....w.......w...
00000080  02 81 01 00 00 00 50 a2 00 00 00 00 00 00 00 00  ......P.........
00000090  50 a4 86 3d 78 00 00 00 60 a5 8d 3d 78 00 00 00  P..=x...`..=x...
000000a0  10 ce 8d 3d 78 00 00 00 00 00 00 00 00 00 00 00  ...=x...........
000000b0  02 81 01 00 00 00 d6 24 00 00 00 00 00 00 00 00  .......$........
000000c0  a0 0b 96 3d 78 00 00 00 c0 40 56 3d 78 00 00 00  ...=x....@V=x...
000000d0  40 c8 84 3d 78 00 00 00 00 00 00 00 00 00 00 00  @..=x...........
000000e0  02 81 01 00 00 00 7d 67 00 00 00 00 00 00 00 00  ......}g........
000000f0  90 fa aa 3d 78 00 00 00 e0 5e 96 3d 78 00 00 00  ...=x....^.=x...

Les deux autres arguments sont respectivement la taille du message clair, le nonce et la taille du nonce :

param2: 0x12
param3 addr: 0x783d8dd620
param3 content:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  1f bf cd 18 44 ff f1 7f 03 ba cf 24 e6 0a 7e d3  ....D......$..~.
00000010  ba 62 c9 8d 95 c1 7c c5 98 8e 9f da 77 00 00 00  .b....|.....w...
00000020  02 01 02 00 00 00 a1 a0 00 00 00 00 00 00 00 00  ................
00000030  02 00 00 00 00 00 00 00 90 cb c7 da 77 00 00 00  ............w...
00000040  a0 cb c7 da 77 00 00 00 a0 cb c7 da 77 00 00 00  ....w.......w...
00000050  02 81 01 00 00 00 69 94 00 00 00 00 00 00 00 00  ......i.........
00000060  f0 3d 89 3d 78 00 00 00 90 7e 8a 3d 78 00 00 00  .=.=x....~.=x...
00000070  60 d1 85 3d 78 00 00 00 00 00 00 00 00 00 00 00  `..=x...........
00000080  02 01 02 00 00 00 3e d3 00 00 00 00 00 00 00 00  ......>.........
00000090  02 00 00 00 00 00 00 00 30 91 90 da 77 00 00 00  ........0...w...
000000a0  40 91 90 da 77 00 00 00 40 91 90 da 77 00 00 00  @...w...@...w...
000000b0  02 01 02 00 00 00 c3 17 00 00 00 00 00 00 00 00  ................
000000c0  02 00 00 00 00 00 00 00 30 db 84 da 77 00 00 00  ........0...w...
000000d0  40 db 84 da 77 00 00 00 40 db 84 da 77 00 00 00  @...w...@...w...
000000e0  02 81 01 00 00 00 b6 c4 00 00 00 00 00 00 00 00  ................
000000f0  e0 25 8d 3d 78 00 00 00 10 1b 8f 3d 78 00 00 00  .%.=x......=x...
param4: 0x18

En continuant de tirer la pelote, on trouve indirectement trace du message clair dans le 3ème argument de utopia::PrivateMessage::sendInstantMessage(utopia::AddressInfo const&, utopia::PrivateMessageInfo const&),

appelante de utopia::crypto::encrypt(1, 2, 3, 4).

Ce troisième argument pointe sur une structure :

param 2 addr = 0x75ab0b3098, content:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  d0 a3 42 b9 77 00 00 00 50 f9 3f 19 79 00 00 00  ..B.w...P.?.y...
00000010  40 99 39 ca 75 00 00 00 d0 30 0b ab 75 00 00 00  @.9.u....0..u...
00000020  04 00 00 00 05 00 00 00 01 00 00 00 00 00 00 00  ................

Les 8 premiers octets de cette structure contiennent une adresse où l’on peut trouver le message :

param2->offset0 = 0x77b942a3d0, content:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  07 00 00 00 00 00 00 00 33 3f 1d b8 12 78 41 0f  ........3?...xA.
00000010  00 f1 aa 3d 78 00 00 00 01 00 61 74 65 5f 6d 65  ...=x.....ate_me
00000020  24 73 61 6e 64 77 69 63 68 20 61 75 20 6a 61 6d  $sandwich au jam
00000030  62 6f 6e 00 00 00 00 00 08 74 65 78 74 00 65 73  bon......text.es
00000040  73 61 67 65 5f 69 6e 66 6f 22 2e 22 69 64 22 20  sage_info"."id" 
00000050  40 9f d8 0b 9c 01 00 00 00 00 00 00 00 00 00 00  @...............

Schématiquement, on a donc ceci :

Assez étrangement, la clé de chiffrement n’est pas présente dans les arguments de ces deux fonctions.

On en retrouve toutefois trace dans les registres x4 et x9 lors de l’appel à utopia::crypto::encrypt(1, 2, 3, 4) :

content at x4:            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  4d b8 0f 3e a4 74 12 eb ca 43 62 f9 e2 19 cd 4e  M..>.t...Cb....N
00000010  aa 8c 0b 10 5b 0d 76 27 16 ba 4c b4 64 56 71 34  ....[.v'..L.dVq4
(...)
content at x9:            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  4d b8 0f 3e a4 74 12 eb ca 43 62 f9 e2 19 cd 4e  M..>.t...Cb....N
00000010  aa 8c 0b 10 5b 0d 76 27 16 ba 4c b4 64 56 71 34  ....[.v'..L.dVq4
(...)
utopia::crypto::encrypt(1, 2, 3, 4) (thread n°25266) called from:

Le message chiffré, quant à lui, peut être observé dans la sortie de crypto_secretbox_xsalsa20poly1305 :

crypto_secretbox_xsalsa20poly1305() (thread n°25266) output:
plaintext addr = 0x7879506640
plaintext content:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000020  73 61 6e 64 77 69 63 68 20 61 75 20 6a 61 6d 62  sandwich au jamb
00000030  6f 6e                                            on
ciphertext addr = 0x78792639b0
ciphertext content:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000010  9b a1 47 45 60 ec 93 af de c5 7b 27 08 6b 2e a4  ..GE`.....{'.k..
00000020  37 72 6a 0b 37 d8 70 df 54 be d9 15 bc a2 4d 4b  7rj.7.p.T.....MK
00000030  69 be 00 00 00 00 00 00 65 00 00 00 00 00 00 00  i.......e.......
00000040  04 01 04 00 00 00 e3 aa 00 00 00 00 00 00 00 00  ................
00000050  00 00 00 00 00 00 00 00 00 00 87 44 00 00 00 00  ...........D....
00000060  00 00 87 44 00 00 16 45 00 00 00 00 00 00 16 45  ...D...E.......E
00000070  e0 59                                            .Y

On le retrouve dans un appel ultérieur à crypto_secretbox_xsalsa20poly1305, qui n’est pas réalisé par utopia::crypto::encrypt(1, 2, 3, 4)

mais par utopia::crypto::encrypt(1, 2, 3, 4, 5, 6) :

crypto_secretbox_xsalsa20poly1305() (call n°1) called from:
0x75cb1d2558 libUtopia_arm64-v8a.so!crypto_box_curve25519xsalsa20poly1305_afternm+0x38
0x75cac814e4 libUtopia_arm64-v8a.so!_ZN6utopia6crypto7encryptENSt6__ndk14spanIKhLm18446744073709551615EEES4_S4_RNS_5utils9ByteArrayES7_S7_+0x240
0x75cae69a20 libUtopia_arm64-v8a.so!_ZN6utopia3msg12CommandLevel11setUserDataERKNS_5utils9ByteArrayES5_S5_+0x13c
0x75cae69a20 libUtopia_arm64-v8a.so!_ZN6utopia3msg12CommandLevel11setUserDataERKNS_5utils9ByteArrayES5_S5_+0x13c
0x75ca24822c libUtopia_arm64-v8a.so!_ZN6utopia14PrivateMessage4sendERKNS_11AddressInfoEjRKNS_3msg9UserLevelE+0x354
0x75ca2cb4f4 libUtopia_arm64-v8a.so!_ZNSt6__ndk116__variant_detail18__copy_constructorINS0_8__traitsIJhtjmN6utopia5utils9ByteArrayEEEELNS0_6_TraitE1EED2Ev+0x18
0x75ca2cb488 libUtopia_arm64-v8a.so!_ZNSt6__ndk116__variant_detail17__copy_assignmentINS0_8__traitsIJhtjmN6utopia5utils9ByteArrayEEEELNS0_6_TraitE1EED2Ev+0x18
0x75ca6c17b0 libUtopia_arm64-v8a.so!0x8ea97b0
0x75ca5e3364 libUtopia_arm64-v8a.so!0x8dcb364
0x75ca6c1664 libUtopia_arm64-v8a.so!_ZN6utopia14ProfileStorage4Priv20updatePrivateMessageERKNS_18PrivateMessageInfoENS2_5FieldE+0x44
0x75ca3782a4 libUtopia_arm64-v8a.so!0x8b602a4
0x75ca248dc4 libUtopia_arm64-v8a.so!_ZN6utopia14PrivateMessage18sendInstantMessageERKNS_11AddressInfoERKNS_18PrivateMessageInfoE+0x68c
0x75ca248db0 libUtopia_arm64-v8a.so!_ZN6utopia14PrivateMessage18sendInstantMessageERKNS_11AddressInfoERKNS_18PrivateMessageInfoE+0x678
0x75856b37b4
0x75856e1b4c
0x75ca2ac47c libUtopia_arm64-v8a.so!0x8a9447c

crypto_secretbox_xsalsa20poly1305() (thread n°25266) input:
ciphertext buffer addr:0x77695076e0
plaintext buffer content (addr = 0x7769518d40):
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000020  00 31 00 00 00 04 00 00 00 03 00 32 00 00 00 18  .1.........2....
00000030  1f bf cd 18 44 ff f1 7f 03 ba cf 24 e6 0a 7e d3  ....D......$..~.
00000040  ba 62 c9 8d 95 c1 7c c5 10 91 00 00 00 08 0f 41  .b....|........A
00000050  78 12 b8 1d 3f 33 10 92 00 00 00 22 9b a1 47 45  x...?3....."..GE
00000060  60 ec 93 af de c5 7b 27 08 6b 2e a4 37 72 6a 0b  `.....{'.k..7rj.
00000070  37 d8 70 df 54 be d9 15 bc a2 4d 4b 69 be 10 96  7.p.T.....MKi...

Le message est donc chiffré (au moins) deux fois avant d’être envoyé sur le réseau.

Cela donne un petit aperçu du fonctionnement de cette application sur laquelle on pourrait passer des mois… mais intéressons-nous plutôt au mécanisme de chiffrement utilisé, XSalsa20.

L’algorithme Salsa20

L’algorithme Salsa20 est un algorithme de chiffrement par flot créé en 2005 par le cryptologue Daniel Bernstein dans le cadre du concours eSTREAM.

Ce concours, ayant eu lieu entre fin 2004 et début 2008 visait à établir un choisir un nouvel algorithme de chiffrement par flot, un peu comme le processus ayant permis de sélectionner l’algorithme AES.

Son design est très simple, puisqu’il n’utilise que trois opérations, toutes sur des entiers de 32 bits :

  • addition modulo 2 ^ 32
  • rotation
  • xor bit à bit.

À la différence de la plupart des algorithmes de chiffrement par bloc comme AES, Salsa20 n’utilise pas de S-box, ce qui évite certains problèmes :

  • La présence d’une S-box induit des accès à des caches. Des cache-timing attacks sont alors possibles lorsque AES est utilisé dans un contexte où des attaques physiques sont possibles.
  • Lorsque l’on veut masquer l’algorithme utilisé (obfuscation d’un malware, cryptographie en boite blanche…), la présence d’une S-box dans l’exécutable ou dans la mémoire du processus étudié trahit l’algorithme utilisé.

Comme tout algorithme de chiffrement par flot, Salsa20 produit une suite de bits (on parle de « keystream ») xorés avec les bits de clair. Le keystream est produit en en « touillant » un état interne.

Salsa20 utilise un état interne qui prend la forme d’une matrice 4 x 4 d’entiers de 32 bits.

L’état initial de cette matrice est constitué à partir de la clé de 256 bits, du nonce de 64 bits, d’un index de 64 bits et d’une suite de caractères fixe de 128 bits :

L’état interne de Salsa20 est produit en appliquant des transformations à cet état initial.

Ces transformations repose sur la transformation élémentaire suivante :

Le code de cette transformation ressemble à ceci :

#define ROTL32(a, b)    (((a) << (b)) | ((a) >> (32 - (b))))

#define QROUND(a, b, c, d)
{
    a += b; d ^= a; d = ROTL32(d, 16);
    c += d; b ^= c; b = ROTL32(b, 12);
    a += b; d ^= a; d = ROTL32(d,  8);
    c += d; b ^= c; b = ROTL32(b,  7);
}

Les tours pairs mettent à jours les lignes de l’état :

QROUND(0,   1,  2,  3);
QROUND(5,   6,  7,  4);
QROUND(10, 11,  8,  9);
QROUND(15, 12, 13, 14);

ALors que les tours impairs mettent à jour les colonnes de l’état :

QROUND(0,   4,   8,  12);
QROUND(5,   9,  13,   1);
QROUND(10, 14,   2,   6);
QROUND(15,  3,   7,  11);

Salsa20 effectue 20 tours pour transformer son état interne.

Après cette dernière étape, l’état interne transformé est xoré avec une sauvegarde de l’état interne pour produire les bits de keystream :

X = Y

for (i = 0; i < 10; i++)
{
    Y  =  ODD_ROUND_TRANSFORM(Y);
    Y  = EVEN_ROUND_TRANSFORM(Y);
}

for (i = 0; i < 16; i++)
{
    OUT[i] ^= X[i];
}

À l’heure actuelle (fin février 2026), les meilleures cryptanalyses de Salsa20 portent sur des variantes affaiblies à 5, 6, 7 ou 8 tours et sont irréalisables en pratique.

XSalsa20

XSalsa20 est une variante de Salsa20 utilisant un nonce de 192 bits au lieu de 64.

XSalsa20 est identique à Salsa20, modulo une étape préliminaire chargée de « digérer » les 128 premiers bits de nonce.

Cette étape se déroule ainsi :

  • construction d’un état de 4 x 4 mots de 32 bits. Les 4 mots diagonaux d’indice 0, 5, 10 et 15 sont les constantes de Salsa20 (« expa », « nd 3 », « 2-by » et « te k »).
  • les mots d’indice 6, 7, 8 et 9 sont les 128 premiers bits du nonce
  • les 256 autres bits sont remplis avec la clef.

Cet état initial est mis à jour avec HSalsa20, qui est identique à Salsa20, excepté l’étape de finale xor :

X = Y
for (i = 0; i < 10; i++)
{
    Y  =  ODD_ROUND_TRANSFORM(Y);
    Y  = EVEN_ROUND_TRANSFORM(Y);
}

L’état résultant Y = (y0,…y15) de HSalsa20 est utilisé avec les 64 bits restantes de nonce et les 64 bits d’index pour initialiser un nouvel état initial :

+--------+--------+--------+--------+
| "expa" |   y0   |   y5   |   y10  |
+--------+--------+--------+--------+
|   y15  | "nd 3" |   n4   |    n5  |
+--------+--------+--------+--------+
|    c1  |    c2  | "2-by" |    y6  |
+--------+--------+--------+--------+
|    y7  |    y8  |   y9   | "te k" |
+--------+--------+--------+--------+

où (n4, n5) sont les 64 derniers bits de nonce et (c1, c2) les 64 bits d’index.

Chacha20

Chacha20 est une variante de Salsa20 créée en 2008 par le concepteur de Salsa20. Il partage de nombreux points communs avec Salsa20 : État initial de 512 bits formé de 256 bits de clé, de 64 bits de nonce, de 128 bits de constante et de 64 bits d’index, mise à jour de l’état interne avec une fonction constituée de 20 tours.

Cependant, la fonction QROUND diffère de celle de Salsa20, et les tours impairs ne mettent pas à jour les 4 lignes, mais les 4 diagonales de l’état interne.

Chacha20 est largement utilisé puisqu’il est l’algorithme de chiffrement d’une des cinq cipher_suites de TLSv1.3 (la suite TLS_CHACHA20_POLY1305_SHA256, les quatre autres utilisant AES).

L’algorithme Poly1305

Poly1305 est un algorithme de mac inventé par Daniel Bernstein (encore lui !) en 2002. L’algorithme est aussi simple qu’élégant, puisqu’il consiste à créer un constituer un polynôme à partir du message à authentifier, et d’évaluer ce polynôme sur la clef.

Rentrons un peu dans les détails : Supposons que l’on souhaite authentifier un message m avec une clé de 256 bits, que l’on coupe en 2 moitiés, r et s.

  • On découpe le message en q blocs de 16 octets,
  • On interprète chacun de ces q blocs comme un entier en little-endian auquel on rajoute 2^128 (ou 2^l si le bloc ne comporte que l octets non nuls)
  • On interprète ces q blocs comme les coefficients d’un polynôme de degré q ayant un coefficient constant nul
  • On évalue ce polynôme en r et l’on prend le reste modulo le nombre premier 2 ^ 130 – 5 (cela explique le nom Poly1305 !)
  • On additionne le tout avec s modulo 2 ^ 128

Et c’est tout !


Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *