Lorsque l’on parle sécurité des données sur une instance PostgreSQL, nous avons le choix entre le chiffrement “at rest” avec TDE (cf cet article), ou bien l’utilisation de l’extension, bien connue des DBA PostgreSQL, pgcrypto.
Cette extension permet de disposer de fonctions de chiffrement via méthode de hashage et dite de “salage” pour générer des valeurs aléatoires.
Dans cet article, nous allons effectuer une étude comparative sur les différents algorithmes de chiffrement, en portant notre attention sur les temps d’exécution liés au chiffrage, mais aussi les problématiques de stockage que cela peut engendrer.
Etat des lieux
Afin d’effectuer les différents tests, nous partons sur une petite configuration machine, à savoir, une EC2 AWS t2.micro, avec 1 CPU et 1 Go de RAM.
Cette VM héberge une instance de bases de données PostgreSQL version 13.
[postgres@~]$ cat /proc/cpuinfo | egrep -i 'model|Mhz|core' model : 79 model name : Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz cpu MHz : 2299.980 core id : 0 cpu cores : 1
[postgres@ ~]$ cat /proc/meminfo | grep -i Total MemTotal: 834212 kB SwapTotal: 0 kB VmallocTotal: 34359738367 kB HugePages_Total: 0
Nous savons que ce n’est pas avec une telle configuration que nous pouvons nous attendre à pas des performances impressionnantes, mais nous aurons au moins de quoi se faire une idée sur les caractéristiques du chiffrement.
Pour notre jeu de données, nous nous appuyons sur le site “DVD Rental” proposant une base de données fictive de location de DVDs (oui oui cela doit encore exister cette catégorie de société ! ) (DVD Rental sur ce site)
Les tables comportent des données assez représentatives de ce que l’on souhaite faire pour notre étude.
Les tables proposées pour cette base sont les suivantes
(postgres@[local]:5433) [dvdrental] select schemaname,tablename, pg_size_pretty(pg_relation_size(schemaname::varchar||'.'||tablename::varchar)) as Size_table, pg_size_pretty(pg_indexes_size(schemaname::varchar||'.'||tablename::varchar)) as Size_index from pg_tables where schemaname not in ('pg_catalog','information_schema') order by 2 desc; ------------+---------------+------------+------------ public | store | 8192 bytes | 32 kB public | staff | 8192 bytes | 16 kB public | rental | 1200 kB | 1120 kB public | payment | 864 kB | 920 kB public | language | 8192 bytes | 16 kB public | inventory | 200 kB | 208 kB public | film_category | 48 kB | 40 kB public | film_actor | 240 kB | 216 kB public | film | 432 kB | 200 kB public | customer | 72 kB | 112 kB public | country | 8192 bytes | 16 kB public | city | 40 kB | 48 kB public | category | 8192 bytes | 16 kB public | address | 64 kB | 64 kB public | actor | 16 kB | 32 kB
Si l’on souhaite avoir la liste des clients et leurs informations personnelles, y compris leur adresse, nous formulons la requête SQL suivante sur notre outil PostgreSQL client.
(postgres@[local]:5433) [dvdrental] select c.customer_id,c.first_name,c.last_name,c.email,c.create_date,a.address,a.district,a.phone from customer c inner join address a on (c.address_id=a.address_id); customer_id | first_name | last_name | email | create_date | address | district | phone -------------+-------------+--------------+------------------------------------------+-------------+----------------------------------------+----------------------+-------------- 524 | Jared | Ely | jared.ely@sakilacustomer.org | 2006-02-14 | 1003 Qinhuangdao Street | West Java | 35533115997 1 | Mary | Smith | mary.smith@sakilacustomer.org | 2006-02-14 | 1913 Hanoi Way | Nagasaki | 28303384290 2 | Patricia | Johnson | patricia.johnson@sakilacustomer.org | 2006-02-14 | 1121 Loja Avenue | California | 838635286649 3 | Linda | Williams | linda.williams@sakilacustomer.org | 2006-02-14 | 692 Joliet Street | Attika | 448477190408 4 | Barbara | Jones | barbara.jones@sakilacustomer.org | 2006-02-14 | 1566 Inegl Manor | Mandalay | 705814003527 5 | Elizabeth | Brown | elizabeth.brown@sakilacustomer.org | 2006-02-14 | 53 Idfu Parkway | Nantou | 10655648674 6 | Jennifer | Davis | jennifer.davis@sakilacustomer.org | 2006-02-14 | 1795 Santiago de Compostela Way | Texas | 860452626434 7 | Maria | Miller | maria.miller@sakilacustomer.org | 2006-02-14 | 900 Santiago de Compostela Parkway | Central Serbia | 716571220373 8 | Susan | Wilson | susan.wilson@sakilacustomer.org | 2006-02-14 | 478 Joliet Way | Hamilton | 657282285970 9 | Margaret | Moore | margaret.moore@sakilacustomer.org | 2006-02-14 | 613 Korolev Drive | Masqat | 380657522649 10 | Dorothy | Taylor | dorothy.taylor@sakilacustomer.org | 2006-02-14 | 1531 Sal Drive | Esfahan | 648856936185 11 | Lisa | Anderson | lisa.anderson@sakilacustomer.org | 2006-02-14 | 1542 Tarlac Parkway | Kanagawa | 635297277345 12 | Nancy | Thomas | nancy.thomas@sakilacustomer.org | 2006-02-14 | 808 Bhopal Manor | Haryana | 465887807014 13 | Karen | Jackson | karen.jackson@sakilacustomer.org | 2006-02-14 | 270 Amroha Parkway | Osmaniye | 695479687538 14 | Betty | White | betty.white@sakilacustomer.org | 2006-02-14 | 770 Bydgoszcz Avenue | California | 517338314235 15 | Helen | Harris | helen.harris@sakilacustomer.org | 2006-02-14 | 419 Iligan Lane | Madhya Pradesh | 990911107354 16 | Sandra | Martin | sandra.martin@sakilacustomer.org | 2006-02-14 | 360 Toulouse Parkway | England | 949312333307 17 | Donna | Thompson | donna.thompson@sakilacustomer.org | 2006-02-14 | 270 Toulon Boulevard | Kalmykia | 407752414682 ....... (599 rows) Time: 5.196 ms
Mise en place
Comme évoqué ci-dessus, nous utiliserons l’extension “pgcrypto” pour réaliser nos différents tests de chiffrement.
Nous avons la version 1.3 de pgcrypto sur une instance PostgreSQL 13
(postgres@[local]:5433) [dvdrental] \dx Name | Version | Schema | Description ----------+---------+------------+------------------------------ pgcrypto | 1.3 | public | cryptographic functions
Les données que nous nous proposons de traiter seront extraits de la requête avec jointure interne entre la table des clients (customer) et la table des adresses clients (address).
Le plan d’exécution de cette requête est assez simple. 2 lectures séquentielles sont effectuées directement sur les tables heap customer et address dans la mesure ou aucune clause where n’est indiquée.
(postgres@[local]:5433) [dvdrental] explain (analyze, verbose) select c.customer_id,c.first_name,c.last_name,c.email,c.create_date,a.address,a.district,a.phone from customer c inner join address a on (c.address_id=a.address_id); QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------------- Hash Join (cost=21.57..38.14 rows=599 width=94) (actual time=0.978..2.295 rows=599 loops=1) Output: c.customer_id, c.first_name, c.last_name, c.email, c.create_date, a.address, a.district, a.phone Inner Unique: true Hash Cond: (c.address_id = a.address_id) - Seq Scan on public.customer c (cost=0.00..14.99 rows=599 width=55) (actual time=0.009..0.433 rows=599 loops=1) Output: c.customer_id, c.store_id, c.first_name, c.last_name, c.email, c.address_id, c.activebool, c.create_date, c.last_update, c.active - Hash (cost=14.03..14.03 rows=603 width=45) (actual time=0.958..0.961 rows=603 loops=1) Output: a.address, a.district, a.phone, a.address_id Buckets: 1024 Batches: 1 Memory Usage: 56kB - Seq Scan on public.address a (cost=0.00..14.03 rows=603 width=45) (actual time=0.007..0.477 rows=603 loops=1) Output: a.address, a.district, a.phone, a.address_id Planning Time: 0.271 ms Execution Time: 2.705 ms (13 rows) Time: 3.476 ms
Pour cette simple requête, nous n’avons besoin que de 3 millisecondes pour trier les données, avec un coût de 38, et ramener les 599 lignes.
Chiffrement classique
Le chiffrement classique consiste à utiliser les fonctions simples “encrypt / decrypt”.
Ces 2 fonctions utilisent une clé de chiffrement que l’on passe à chacun des champs cryptés lors des ordres INSERT et SELECT.
Tout d’abord on crée les 2 tables vides, copies des tables “customer” et “address”
CREATE TABLE IF NOT EXISTS public.customer_encrypt ( customer_id integer, store_id smallint , first_name bytea , last_name bytea , email bytea, address_id smallint, activebool boolean, create_date date, last_update timestamp without time zone, active integer, CONSTRAINT customer_pkey_crypt PRIMARY KEY (customer_id) ) TABLESPACE pg_default;
CREATE TABLE IF NOT EXISTS public.address_encrypt ( address_id integer, address bytea, address2 character varying(50), district character varying(20), city_id smallint, postal_code character varying(10), phone bytea, last_update timestamp without time zone, CONSTRAINT address_key_crypt PRIMARY KEY (address_id) ) TABLESPACE pg_default;
Puis on y insère les données à partir des tables sources “customer” et “address”. Pour cela, nous utilisons la fonction “encrypt” avec la clé “capdata2023” et l’algorithme aes.
insert into public.customer_encrypt select customer_id, store_id, encrypt(c.first_name::bytea,'capdata2023','aes'), encrypt(c.last_name::bytea,'capdata2023','aes'), encrypt(c.email::bytea,'capdata2023','aes'), address_id, activebool, create_date, last_update, active from customer c; INSERT 0 599 Time: 5.297 ms
insert into public.address_encrypt select address_id, encrypt(address::bytea,'capdata2023','aes'), address2, district, city_id, postal_code, encrypt(phone::bytea,'capdata2023','aes'), last_update from address; INSERT 0 603 Time: 4.616 ms
Les données dans les tables sont bien chiffrées comme le montre le simple SELECT suivant
(postgres@[local]:5433) [dvdrental] select * from customer_encrypt limit 10; customer_id | store_id | first_name | last_name | email | address_id | activebool | create_date | last_update | active -------------+----------+------------------------------------+------------------------------------+----------------------------------------------------------------------------------------------------+------------+------------+-------------+-------------------------+-------- 524 | 1 | \x18a7f8f4ba98111a01f63aef09773202 | \xdf4bcb77c652d6291c34ec7788cbe881 | \xa536a4f46d1bcc00f7183b46b68da712d70a0f419c1e58d778a4e9a3771828d7 | 530 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 1 | 1 | \x4f49ec91c5f38f8d3f751e42966f5695 | \x7c78e265d6e86fc17cdc78ebf1b41dd3 | \xac61ff9165a035a68a821c3959f5867a218fed8e350853e14d0c0a7dcf49b6d5 | 5 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 2 | 1 | \x541dec9d2a1688ff55fe0730e184548b | \x15200a194b068c59b0431271cf6f3c12 | \x7c9b39f5896ac24d8bedc08427adbfe1d5c22379d66dfd2881d3bdabe9b5cae60c329724424be9c6bcfe08cffc356c3c | 6 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 3 | 1 | \x4f1dd6b1d35f3024eb7c6cbf5c600269 | \x47d3a708ba137f5b9a0b873df5a7e340 | \x9297172b1a299f013db248a00d7414c7ea1378fbefc143e3f1f632c431ecf8597b5307f415eb82f672b15449c93c297e | 7 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 4 | 2 | \x84554c6f4c26d2f9c0b832b311263883 | \x6eff33a47393acf9d178afa9553ffae4 | \xe912cb073583fb399c92379d3529b18cf39754828ed28e02e31e3b8c9b523d2c02a18bfebc210bab193d8489cdd11914 | 8 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 5 | 1 | \xca5601a262cbc84669558ba7199f5960 | \x99fc3b33675cb5de00c92447f6f7e8ed | \xae7ea0ea5204c1df2e596af38b53a2da84cce415ae4ba5645df6dba7826e868a26eb59af5e79a7f8a4d55abc1382499b | 9 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 6 | 2 | \xb5f9f526337e730b25f4c4f7dab8eb91 | \x52bba94e13cc36be9f1094c1bae22c47 | \x50c4a57f06048c579897fce47603945db4d7143f6deaa5d801c447f23edde371565e24c4bc53e272e0ebf32ace80fbd5 | 10 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 7 | 1 | \xac6e41d9fb3a3eaefd0775e5b646a730 | \xb8ba78ad82aff0a50717301716e59013 | \xad3cec70e819240d7f7ddd6066364332f0c6b504d15b6627516deb13fe591bd2 | 11 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 8 | 2 | \x8aeb1a151900c8c2b3ac52c3a9c78a42 | \xea0c5c0e6546ffc8da1ef4f04f5addea | \x5eac14927a3c464e8cd88122d02271333e7235e8d212e1c2722a6b3804ba1672 | 12 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1 9 | 2 | \x9248a51a98b10f1c0499f37604e2caa3 | \x254b523f8e750e100218947a2f838585 | \x706c681ae5692ba107cb51c3f54b1ef2699c1e463b68b8fbc48f8f9f15c86a45d456498740e41097f99f46c80837dcbf | 13 | t | 2006-02-14 | 2013-05-26 14:49:45.738 | 1
Pour déchiffrer les données de notre requête SQL avec jointure entre les clients et leurs adresses, nous devons appeler la fonction “decrypt” pour chaque champ, avec la clé de chiffrage associée. Afin de lire correctement la valeur, utiliser la fonction “convert_from” pour transformer l’information de façon lisible.
Ce qui donne la requête suivante :
(postgres@[local]:5433) [dvdrental] select c.customer_id, convert_from(decrypt(c.first_name,'capdata2023','aes'),'UTF8') as first_name, convert_from(decrypt(c.last_name,'capdata2023','aes'),'UTF8') as last_name, convert_from(decrypt(c.email,'capdata2023','aes'),'UTF8') as email, c.create_date, convert_from(decrypt(a.address,'capdata2023','aes'),'UTF8') as adresse, a.district, convert_from(decrypt(a.phone,'capdata2023','aes'),'UTF8') as telephone from customer_encrypt c inner join address_encrypt a on (c.address_id=a.address_id); customer_id | first_name | last_name | email | create_date | adresse | district | telephone -------------+-------------+--------------+------------------------------------------+-------------+----------------------------------------+----------------------+-------------- 524 | Jared | Ely | jared.ely@sakilacustomer.org | 2006-02-14 | 1003 Qinhuangdao Street | West Java | 35533115997 1 | Mary | Smith | mary.smith@sakilacustomer.org | 2006-02-14 | 1913 Hanoi Way | Nagasaki | 28303384290 2 | Patricia | Johnson | patricia.johnson@sakilacustomer.org | 2006-02-14 | 1121 Loja Avenue | California | 838635286649 3 | Linda | Williams | linda.williams@sakilacustomer.org | 2006-02-14 | 692 Joliet Street | Attika | 448477190408 4 | Barbara | Jones | barbara.jones@sakilacustomer.org | 2006-02-14 | 1566 Inegl Manor | Mandalay | 705814003527 5 | Elizabeth | Brown | elizabeth.brown@sakilacustomer.org | 2006-02-14 | 53 Idfu Parkway | Nantou | 10655648674 6 | Jennifer | Davis | jennifer.davis@sakilacustomer.org | 2006-02-14 | 1795 Santiago de Compostela Way | Texas | 860452626434 7 | Maria | Miller | maria.miller@sakilacustomer.org | 2006-02-14 | 900 Santiago de Compostela Parkway | Central Serbia | 716571220373 8 | Susan | Wilson | susan.wilson@sakilacustomer.org | 2006-02-14 | 478 Joliet Way | Hamilton | 657282285970 9 | Margaret | Moore | margaret.moore@sakilacustomer.org | 2006-02-14 | 613 Korolev Drive | Masqat | 380657522649 10 | Dorothy | Taylor | dorothy.taylor@sakilacustomer.org | 2006-02-14 | 1531 Sal Drive | Esfahan | 648856936185 11 | Lisa | Anderson | lisa.anderson@sakilacustomer.org | 2006-02-14 | 1542 Tarlac Parkway | Kanagawa | 635297277345 12 | Nancy | Thomas | nancy.thomas@sakilacustomer.org | 2006-02-14 | 808 Bhopal Manor | Haryana | 465887807014 13 | Karen | Jackson | karen.jackson@sakilacustomer.org | 2006-02-14 | 270 Amroha Parkway | Osmaniye | 695479687538 14 | Betty | White | betty.white@sakilacustomer.org | 2006-02-14 | 770 Bydgoszcz Avenue | California | 517338314235 15 | Helen | Harris | helen.harris@sakilacustomer.org | 2006-02-14 | 419 Iligan Lane | Madhya Pradesh | 990911107354 16 | Sandra | Martin | sandra.martin@sakilacustomer.org | 2006-02-14 | 360 Toulouse Parkway | England | 949312333307 17 | Donna | Thompson | donna.thompson@sakilacustomer.org | 2006-02-14 | 270 Toulon Boulevard | Kalmykia | 407752414682 ... (599 rows) Time: 5.064 ms
Chiffrement PGP symétrique
Le chiffrement PGP (Pretty Good Privacy) utilise des standards liés à OpenPGP (RFC 2440).
– Un paquet est envoyé avec la clé de session (clé symétrique ou bien une clé publique)
– Paquet contenant les données chiffrées avec la clé de session.
Avec un chiffrement à clé symétrique, le mot de passe est envoyé crypté avec l’algorithme String2key (S2K).
Puis elles sont chiffrées avec la clé de session.
CREATE TABLE IF NOT EXISTS public.customer_pgp_sym ( customer_id integer, store_id smallint , first_name bytea , last_name bytea , email bytea, address_id smallint, activebool boolean, create_date date, last_update timestamp without time zone, active integer, CONSTRAINT customer_pkey_sym PRIMARY KEY (customer_id) ) TABLESPACE pg_default;
CREATE TABLE IF NOT EXISTS public.address_pgp_sym ( address_id integer, address bytea, address2 character varying(50), district character varying(20), city_id smallint, postal_code character varying(10), phone bytea, last_update timestamp without time zone, CONSTRAINT address_key_sym PRIMARY KEY (address_id) ) TABLESPACE pg_default;
insert into public.customer_pgp_sym_256 select customer_id, store_id, pgp_sym_encrypt(c.first_name,'capdata2023','cipher-algo=aes256, compress-algo=1, sess-key=1'), pgp_sym_encrypt(c.last_name,'capdata2023','cipher-algo=aes256, compress-algo=1, sess-key=1'), pgp_sym_encrypt(c.email,'capdata2023','cipher-algo=aes256, compress-algo=1, sess-key=1'), address_id, activebool, create_date, last_update, active from customer c; INSERT 0 599 Time: 1822.944 ms (00:01.823)
insert into public.address_pgp_sym_256 select address_id, pgp_sym_encrypt(address,'capdata2023','cipher-algo=aes256, compress-algo=1, sess-key=1'), address2, district, city_id, postal_code, pgp_sym_encrypt(phone,'capdata2023','cipher-algo=aes256, compress-algo=1, sess-key=1'), last_update from address; INSERT 0 603 Time: 1244.807 ms (00:01.245)
Nous dépassons la seconde à chaque opération d’insertions dans les nouvelles tables !!
Si l’on souhaite décrypter les champs , nous utilisons la fonction “pgp_sym_decrypt” et notre clé de chiffrement.
select c.customer_id, pgp_sym_decrypt(c.first_name,'capdata2023') as first_name, pgp_sym_decrypt(c.last_name,'capdata2023') as last_name, pgp_sym_decrypt(c.email,'capdata2023') as email, c.create_date, pgp_sym_decrypt(a.address,'capdata2023') as adresse, a.district, pgp_sym_decrypt(a.phone,'capdata2023') as telephone from customer_pgp_sym_256 c inner join address_pgp_sym_256 a on (c.address_id=a.address_id); customer_id | first_name | last_name | email | create_date | adresse | district | telephone -------------+-------------+--------------+------------------------------------------+-------------+----------------------------------------+----------------------+-------------- 524 | Jared | Ely | jared.ely@sakilacustomer.org | 2006-02-14 | 1003 Qinhuangdao Street | West Java | 35533115997 1 | Mary | Smith | mary.smith@sakilacustomer.org | 2006-02-14 | 1913 Hanoi Way | Nagasaki | 28303384290 2 | Patricia | Johnson | patricia.johnson@sakilacustomer.org | 2006-02-14 | 1121 Loja Avenue | California | 838635286649 3 | Linda | Williams | linda.williams@sakilacustomer.org | 2006-02-14 | 692 Joliet Street | Attika | 448477190408 4 | Barbara | Jones | barbara.jones@sakilacustomer.org | 2006-02-14 | 1566 Inegl Manor | Mandalay | 705814003527 5 | Elizabeth | Brown | elizabeth.brown@sakilacustomer.org | 2006-02-14 | 53 Idfu Parkway | Nantou | 10655648674 6 | Jennifer | Davis | jennifer.davis@sakilacustomer.org | 2006-02-14 | 1795 Santiago de Compostela Way | Texas | 860452626434 7 | Maria | Miller | maria.miller@sakilacustomer.org | 2006-02-14 | 900 Santiago de Compostela Parkway | Central Serbia | 716571220373 8 | Susan | Wilson | susan.wilson@sakilacustomer.org | 2006-02-14 | 478 Joliet Way | Hamilton | 657282285970 9 | Margaret | Moore | margaret.moore@sakilacustomer.org | 2006-02-14 | 613 Korolev Drive | Masqat | 380657522649 10 | Dorothy | Taylor | dorothy.taylor@sakilacustomer.org | 2006-02-14 | 1531 Sal Drive | Esfahan | 648856936185 11 | Lisa | Anderson | lisa.anderson@sakilacustomer.org | 2006-02-14 | 1542 Tarlac Parkway | Kanagawa | 635297277345 12 | Nancy | Thomas | nancy.thomas@sakilacustomer.org | 2006-02-14 | 808 Bhopal Manor | Haryana | 465887807014 13 | Karen | Jackson | karen.jackson@sakilacustomer.org | 2006-02-14 | 270 Amroha Parkway | Osmaniye | 695479687538 14 | Betty | White | betty.white@sakilacustomer.org | 2006-02-14 | 770 Bydgoszcz Avenue | California | 517338314235 15 | Helen | Harris | helen.harris@sakilacustomer.org | 2006-02-14 | 419 Iligan Lane | Madhya Pradesh | 990911107354 16 | Sandra | Martin | sandra.martin@sakilacustomer.org | 2006-02-14 | 360 Toulouse Parkway | England | 949312333307 ..... (599 rows) Time: 2887.341 ms (00:02.887)
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Hash Join (cost=37.57..84.63 rows=599 width=177) (actual time=7.387..2886.456 rows=599 loops=1) Output: c.customer_id, pgp_sym_decrypt(c.first_name, 'capdata2023'::text), pgp_sym_decrypt(c.last_name, 'capdata2023'::text), pgp_sym_decrypt(c.email, 'capdata2023'::text), c.create_date, pgp_sym_decrypt(a.address, 'capdata2023'::text), a.district, pgp_sym_decrypt(a.phone, 'capdata2023'::text) Inner Unique: true Hash Cond: (c.address_id = a.address_id) - Seq Scan on public.customer_pgp_sym_256 c (cost=0.00..37.99 rows=599 width=370) (actual time=0.007..0.833 rows=599 loops=1) Output: c.customer_id, c.store_id, c.first_name, c.last_name, c.email, c.address_id, c.activebool, c.create_date, c.last_update, c.active - Hash (cost=30.03..30.03 rows=603 width=253) (actual time=1.186..1.189 rows=603 loops=1) Output: a.address, a.district, a.phone, a.address_id Buckets: 1024 Batches: 1 Memory Usage: 179kB - Seq Scan on public.address_pgp_sym_256 a (cost=0.00..30.03 rows=603 width=253) (actual time=0.006..0.516 rows=603 loops=1) Output: a.address, a.district, a.phone, a.address_id Planning Time: 0.200 ms Execution Time: 2887.108 ms (13 rows) Time: 2887.897 ms (00:02.888)
Chiffrement PGP avec pair de clés publique/privée
C’est la méthode la plus complexe à mettre en place, mais aussi la plus optimale en termes de sécurité.
Le procédé s’appuie également sur OpenPGP, mais utilise, en plus, une clé publique côté serveur de bases de données. Cette clé publique est utilisée pour chiffrer la donnée.
C’est au déchiffrage que la clé privée est envoyée depuis l’application pour contrôle, et qui sera utilisée pour lire les champs enregistrés.
Gestion des clés RSA
Avant d’utiliser les clés publiques et clés privées, il faut les déclarer sur la machine hébergeant la base de données.
Pour l’installation, nous utilisons l’utilitaire « gpg ». Lancer la création de clés via l’option « full-generate-keys » pour choisir les options de clés. Nous choisissons des clés RSA signées, avec un codage sur 2048 bits. La période de validité des clés est de 2 ans.
Si vous lancez cette commande avec un compte “non root”, exécuter l’option –pinentry-mode=loopback, sinon vous n’aurez pas les permissions de
modifications des clés éventuellement déjà créées.
[postgres@ ~]$ gpg --full-generate-key --pinentry-mode=loopback gpg (GnuPG) 2.2.20; Copyright (C) 2020 Free Software Foundation, Inc. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Please select what kind of key you want: (1) RSA and RSA (default) (2) DSA and Elgamal (3) DSA (sign only) (4) RSA (sign only) (14) Existing key from card Your selection? 1 RSA keys may be between 1024 and 4096 bits long. What keysize do you want? (2048) 2048 Requested keysize is 2048 bits Please specify how long the key should be valid. 0 = key does not expire = key expires in n days w = key expires in n weeks m = key expires in n months y = key expires in n years Key is valid for? (0) 2y Key expires at Wed 09 Jul 2025 02:28:18 PM UTC Is this correct? (y/N) y GnuPG needs to construct a user ID to identify your key. Real name: capdata Email address: Comment: capdata gpg PG test You selected this USER-ID: "capdata (capdata gpg PG test) " Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O pub rsa2048 2023-07-10 [SC] [expires: 2025-07-09] 50B8DAB80C7E568169DCF9A7A38D290FAA943D45 uid capdata (capdata gpg PG test) sub rsa2048 2023-07-10 [E] [expires: 2025-07-09]
Vérifications
[postgres@~]$ gpg --list-keys --keyid-format short gpg: checking the trustdb gpg: marginals needed: 3 completes needed: 1 trust model: pgp gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u gpg: next trustdb check due at 2025-07-09 /var/lib/pgsql/.gnupg/pubring.kbx --------------------------------- pub rsa2048/AA943D45 2023-07-10 [SC] [expires: 2025-07-09] 50B8DAB80C7E568169DCF9A7A38D290FAA943D45 uid [ultimate] capdata (capdata gpg PG test) sub rsa2048/1756306E 2023-07-10 [E] [expires: 2025-07-09]
et
[postgres@~]$ gpg --list-secret-keys --keyid-format short /var/lib/pgsql/.gnupg/pubring.kbx --------------------------------- sec rsa2048/AA943D45 2023-07-10 [SC] [expires: 2025-07-09] 50B8DAB80C7E568169DCF9A7A38D290FAA943D45 uid [ultimate] capdata (capdata gpg PG test) ssb rsa2048/1756306E 2023-07-10 [E] [expires: 2025-07-09]
Exporter la clé publique et la clé privée
[postgres@~]$ gpg --armor --export 'capdata' -----BEGIN PGP PUBLIC KEY BLOCK----- mQENBGSsFaoBCADMenKoGzxL7hZ1gkVHUrY4UXA34ckuZn6yOeSwb/mrY/HdXVtz MO+cCCmJyp0rS3WoWAM+2bcstpAOgJaRzjZMbbKx9P0BocwbahwMbEgGY9J10l6S KT79khDmLrkLuXxiDl3IzsqatqMYIphrdNMZmUYIo5YegX/zcrgY2b3+xRdiPLnk WOmh/hJqzHd9GnTGCDa5jkUrme0DhBHJOBuAyh1abWGHHCrYlrs2guA1iYUMiq3P RD8dkIP5vEZO9XBPWRe0S41Et8kuocn0AlABW0VcA0GenMeviLk/xME2Cpnme9Fr 0FDUxVQYi6vFXuf530G5f9QQnIYsQL26DVa1ABEBAAG0OmNhcGRhdGEgKGNhcGRh dGEgZ3BnIFBHIHRlc3QpIDxlcmFtaUBjYXBkYXRhLW9zbW96aXVtLmNvbT6JAVQE EwEIAD4WIQRQuNq4DH5WgWnc+aejjSkPqpQ9RQUCZKwVqgIbAwUJA8JnAAULCQgH AgYVCgkICwIEFgIDAQIeAQIXgAAKCRCjjSkPqpQ9ReqCB/97Fk6gWWYSfHQoQrH4 N7Rlf1tNN0M5N7gmO6qZQVZzR5qiV1y3ahAIBPyIcQla9Nb3ry1NE5QayZ1FyEnu vTTVF2CWq0yXtes3Sv7Q2DrzoiENVwOeGSxqsx/IqfY8iFL6m3hXwXC51JNraLFh sTP617LKvfSETr+UFpkctdAfgmxlzJ7cUHF+m0lr7OsN9e5XZ8S9CwInFX6GJPDS j/CpUr4l/fdZa5H/1pc+gFBDWoaZYquqoYXM2YHOkPp9RZ12uejbRaIn/SlYKutE....... ...... mkx5lbcQlsaWj8PKM56mCgKF =U1rR -----END PGP PUBLIC KEY BLOCK-----
et
[postgres@~]$ gpg --armor --export-secret-key 'capdata' -----BEGIN PGP PRIVATE KEY BLOCK----- lQOYBGSsFaoBCADMenKoGzxL7hZ1gkVHUrY4UXA34ckuZn6yOeSwb/mrY/HdXVtz MO+cCCmJyp0rS3WoWAM+2bcstpAOgJaRzjZMbbKx9P0BocwbahwMbEgGY9J10l6S KT79khDmLrkLuXxiDl3IzsqatqMYIphrdNMZmUYIo5YegX/zcrgY2b3+xRdiPLnk WOmh/hJqzHd9GnTGCDa5jkUrme0DhBHJOBuAyh1abWGHHCrYlrs2guA1iYUMiq3P RD8dkIP5vEZO9XBPWRe0S41Et8kuocn0AlABW0VcA0GenMeviLk/xME2Cpnme9Fr 0FDUxVQYi6vFXuf530G5f9QQnIYsQL26DVa1ABEBAAEAB/4poUVeJdtfDxxZ9LmD lZqdOTFaYzjZHkttoD1H0ahYZUr8+VCQ0XX7A6tnTw20HpMYAME6Zst1Cj8mgLYG /d+OrGfM9Nac4jLCoxYOTm5UhLa4v6l64vRc3kPcBUet1Cf3c7rS0w0rNgNa+tIi 0IBZDhxUzm9WCyIAb8r83jnhGCSTaAeCBOqHnXE+JgUOdf15k1oVxYZdS73mpoNT aLXmmJbC7JFC74j40oP4brbUzzWo0mZo0R394ZG0booBJM2BDH4ydrSvGWbsreSF jo31xkHqsLOPHDlJvdVnbWVSuyjk0oL2bKWgXTz9oT1YxiaN32WRgM6cMXvXHZka TxhhBADa3SQQjK5+QXRYcTZjnMZ+E15AIk/DklYC1/Q/TYtKKD3w7oaoc7M8sPsq w7gzTMc9kAKb7NlG4x+v4+Ab5RuV08osS7ZPu3H3gZyPgjjyjwZEKG0pnMJdDfwr eWqhIdc35gd8FGiNHgoeFVS9pAA5TBB3W+mgR3x9Jdj/Q8htlQQA7yxwQ3/1bYyj ejd1Hvihq4YtvMyA7pJSkv5ptqyA/qiM6jkjH4WVL4Qew+IrmHOCfVyEjebIQgF...... ...... 5YLh06oFmkx5lbcQlsaWj8PKM56mCgKF =fYKN -----END PGP PRIVATE KEY BLOCK-----
La suite consistera à créer les tables pour accueillir ces données chiffrées, et insérer les données à partir des données sources.
CREATE TABLE IF NOT EXISTS public.customer_pgp_pub ( customer_id integer, store_id smallint , first_name bytea , last_name bytea , email bytea, address_id smallint, activebool boolean, create_date date, last_update timestamp without time zone, active integer, CONSTRAINT customer_pkey_pub PRIMARY KEY (customer_id) ) TABLESPACE pg_default; CREATE TABLE IF NOT EXISTS public.address_pgp_pub ( address_id integer, address bytea, address2 character varying(50), district character varying(20), city_id smallint, postal_code character varying(10), phone bytea, last_update timestamp without time zone, CONSTRAINT address_key_pub PRIMARY KEY (address_id) ) TABLESPACE pg_default;
Lors de l’étape d’insertion de données, nous devons renseigner la clé publique, et utiliser, sur chaque champ, la fonction “pgp_pub_encrypt” !
insert into public.customer_pgp_pub select customer_id, store_id, pgp_pub_encrypt(c.first_name, keys.pubkey), pgp_pub_encrypt(c.last_name,keys.pubkey), pgp_pub_encrypt(c.email,keys.pubkey), address_id, activebool, create_date, last_update, active from customer c cross join (select dearmor('-----BEGIN PGP PUBLIC KEY BLOCK----- mQENBGSsFaoBCADMenKoGzxL7hZ1gkVHUrY4UXA34ckuZn6yOeSwb/mrY/HdXVtz MO+cCCmJyp0rS3WoWAM+2bcstpAOgJaRzjZMbbKx9P0BocwbahwMbEgGY9J10l6S KT79khDmLrkLuXxiDl3IzsqatqMYIphrdNMZmUYIo5YegX/zcrgY2b3+xRdiPLnk WOmh/hJqzHd9GnTGCDa5jkUrme0DhBHJOBuAyh1abWGHHCrYlrs2guA1iYUMiq3P RD8dkIP5vEZO9XBPWRe0S41Et8kuocn0AlABW0VcA0GenMeviLk/xME2Cpnme9Fr 0FDUxVQYi6vFXuf530G5f9QQnIYsQL26DVa1ABEBAAG0OmNhcGRhdGEgKGNhcGRh dGEgZ3BnIFBHIHRlc3QpIDxlcmFtaUBjYXBkYXRhLW9zbW96aXVtLmNvbT6JAVQE EwEIAD4WIQRQuNq4DH5WgWnc+aejjSkPqpQ9RQUCZKwVqgIbAwUJA8JnAAULCQgH AgYVCgkICwIEFgIDAQIeAQIXgAAKCRCjjSkPqpQ9ReqCB/97Fk6gWWYSfHQoQrH4..... .... vrti5S7AxozOn3jUfUawgKGHRhY2/Sm06+dmThnV3O3jRqAJcerTcFyL5YLh06oF mkx5lbcQlsaWj8PKM56mCgKF =U1rR -----END PGP PUBLIC KEY BLOCK-----') As pubkey) As keys; INSERT 0 599 Time: 159.320 ms
insert into public.address_pgp_pub select address_id, pgp_pub_encrypt(address,keys.pubkey), address2, district, city_id, postal_code, pgp_pub_encrypt(phone,keys.pubkey), last_update from address cross join (select dearmor('-----BEGIN PGP PUBLIC KEY BLOCK----- mQENBGSsFaoBCADMenKoGzxL7hZ1gkVHUrY4UXA34ckuZn6yOeSwb/mrY/HdXVtz MO+cCCmJyp0rS3WoWAM+2bcstpAOgJaRzjZMbbKx9P0BocwbahwMbEgGY9J10l6S KT79khDmLrkLuXxiDl3IzsqatqMYIphrdNMZmUYIo5YegX/zcrgY2b3+xRdiPLnk WOmh/hJqzHd9GnTGCDa5jkUrme0DhBHJOBuAyh1abWGHHCrYlrs2guA1iYUMiq3P RD8dkIP5vEZO9XBPWRe0S41Et8kuocn0AlABW0VcA0GenMeviLk/xME2Cpnme9Fr 0FDUxVQYi6vFXuf530G5f9QQnIYsQL26DVa1ABEBAAG0OmNhcGRhdGEgKGNhcGRh dGEgZ3BnIFBHIHRlc3QpIDxlcmFtaUBjYXBkYXRhLW9zbW96aXVtLmNvbT6JAVQE EwEIAD4WIQRQuNq4DH5WgWnc+aejjSkPqpQ9RQUCZKwVqgIbAwUJA8JnAAULCQgH AgYVCgkICwIEFgIDAQIeAQIXgAAKCRCjjSkPqpQ9ReqCB/97Fk6gWWYSfHQoQrH4 N7Rlf1tNN0M5N7gmO6qZQVZzR5qiV1y3ahAIBPyIcQla9Nb3ry1NE5QayZ1FyEnu .... vrti5S7AxozOn3jUfUawgKGHRhY2/Sm06+dmThnV3O3jRqAJcerTcFyL5YLh06oF mkx5lbcQlsaWj8PKM56mCgKF =U1rR -----END PGP PUBLIC KEY BLOCK-----') As pubkey)  As keys; INSERT 0 599 Time: 159.320 ms
select c.customer_id, pgp_pub_decrypt(c.first_name,keys.pubkey) as first_name, pgp_pub_decrypt(c.last_name,keys.pubkey) as last_name, pgp_pub_decrypt(c.email,keys.pubkey) as email, c.create_date, pgp_pub_decrypt(a.address,keys.pubkey) as adresse, a.district, pgp_pub_decrypt(a.phone,keys.pubkey) as telephone from customer_pgp_pub c inner join address_pgp_pub a on (c.address_id=a.address_id) cross join (SELECT dearmor('-----BEGIN PGP PRIVATE KEY BLOCK----- lQOYBGSsFaoBCADMenKoGzxL7hZ1gkVHUrY4UXA34ckuZn6yOeSwb/mrY/HdXVtz MO+cCCmJyp0rS3WoWAM+2bcstpAOgJaRzjZMbbKx9P0BocwbahwMbEgGY9J10l6S KT79khDmLrkLuXxiDl3IzsqatqMYIphrdNMZmUYIo5YegX/zcrgY2b3+xRdiPLnk WOmh/hJqzHd9GnTGCDa5jkUrme0DhBHJOBuAyh1abWGHHCrYlrs2guA1iYUMiq3P RD8dkIP5vEZO9XBPWRe0S41Et8kuocn0AlABW0VcA0GenMeviLk/xME2Cpnme9Fr 0FDUxVQYi6vFXuf530G5f9QQnIYsQL26DVa1ABEBAAEAB/4poUVeJdtfDxxZ9LmD lZqdOTFaYzjZHkttoD1H0ahYZUr8+VCQ0XX7A6tnTw20HpMYAME6Zst1Cj8mgLYG /d+OrGfM9Nac4jLCoxYOTm5UhLa4v6l64vRc3kPcBUet1Cf3c7rS0w0rNgNa+tIi 0IBZDhxUzm9WCyIAb8r83jnhGCSTaAeCBOqHnXE+JgUOdf15k1oVxYZdS73mpoNT aLXmmJbC7JFC74j40oP4brbUzzWo0mZo0R394ZG0booBJM2BDH4ydrSvGWbsreSF jo31xkHqsLOPHDlJvdVnbWVSuyjk0oL2bKWgXTz9oT1YxiaN32WRgM6cMXvXHZka TxhhBADa3SQQjK5+QXRYcTZjnMZ+E15AIk/DklYC1/Q/TYtKKD3w7oaoc7M8sPsq w7gzTMc9kAKb7NlG4x+v4+Ab5RuV08osS7ZPu3H3gZyPgjjyjwZEKG0pnMJdDfwr..... ..... Nd51J9eivrti5S7AxozOn3jUfUawgKGHRhY2/Sm06+dmThnV3O3jRqAJcerTcFyL 5YLh06oFmkx5lbcQlsaWj8PKM56mCgKF =fYKN -----END PGP PRIVATE KEY BLOCK-----') As pubkey) As keys; customer_id | first_name | last_name | email | create_date | adresse | district | telephone -------------+-------------+--------------+------------------------------------------+-------------+----------------------------------------+----------------------+-------------- 524 | Jared | Ely | jared.ely@sakilacustomer.org | 2006-02-14 | 1003 Qinhuangdao Street | West Java | 35533115997 1 | Mary | Smith | mary.smith@sakilacustomer.org | 2006-02-14 | 1913 Hanoi Way | Nagasaki | 28303384290 2 | Patricia | Johnson | patricia.johnson@sakilacustomer.org | 2006-02-14 | 1121 Loja Avenue | California | 838635286649 3 | Linda | Williams | linda.williams@sakilacustomer.org | 2006-02-14 | 692 Joliet Street | Attika | 448477190408 4 | Barbara | Jones | barbara.jones@sakilacustomer.org | 2006-02-14 | 1566 Inegl Manor | Mandalay | 705814003527 5 | Elizabeth | Brown | elizabeth.brown@sakilacustomer.org | 2006-02-14 | 53 Idfu Parkway | Nantou | 10655648674 6 | Jennifer | Davis | jennifer.davis@sakilacustomer.org | 2006-02-14 | 1795 Santiago de Compostela Way | Texas | 860452626434 7 | Maria | Miller | maria.miller@sakilacustomer.org | 2006-02-14 | 900 Santiago de Compostela Parkway | Central Serbia | 716571220373 8 | Susan | Wilson | susan.wilson@sakilacustomer.org | 2006-02-14 | 478 Joliet Way | Hamilton | 657282285970 9 | Margaret | Moore | margaret.moore@sakilacustomer.org | 2006-02-14 | 613 Korolev Drive | Masqat | 380657522649 10 | Dorothy | Taylor | dorothy.taylor@sakilacustomer.org | 2006-02-14 | 1531 Sal Drive | Esfahan | 648856936185 11 | Lisa | Anderson | lisa.anderson@sakilacustomer.org | 2006-02-14 | 1542 Tarlac Parkway | Kanagawa | 635297277345 12 | Nancy | Thomas | nancy.thomas@sakilacustomer.org | 2006-02-14 | 808 Bhopal Manor | Haryana | 465887807014 13 | Karen | Jackson | karen.jackson@sakilacustomer.org | 2006-02-14 | 270 Amroha Parkway | Osmaniye | 695479687538 14 | Betty | White | betty.white@sakilacustomer.org | 2006-02-14 | 770 Bydgoszcz Avenue | California | 517338314235 15 | Helen | Harris | helen.harris@sakilacustomer.org | 2006-02-14 | 419 Iligan Lane | Madhya Pradesh | 990911107354 16 | Sandra | Martin | sandra.martin@sakilacustomer.org | 2006-02-14 | 360 Toulouse Parkway | England | 949312333307 ...... (599 rows) Time: 11351.879 ms (00:11.352)
Des données relevées en 11 secondes. Concernant le plan d’exécution, , le coût global de notre jointure double vis à vis du chiffrement symétrique (on passe à 175), et un temps d’exécution bien plus long (11400 millisecondes pour la jointure).
Hash Join (cost=74.57..175.63 rows=599 width=177) (actual time=21.263..11498.855 rows=599 loops=1) Output: c.customer_id, pgp_pub_decrypt(c.first_name, \************************************ Inner Unique: true Hash Cond: (c.address_id = a.address_id) - Seq Scan on public.customer_pgp_pub c (cost=0.00..91.99 rows=599 width=1030) (actual time=0.006..1.294 rows=599 loops=1) Output: c.customer_id, c.store_id, c.first_name, c.last_name, c.email, c.address_id, c.activebool, c.create_date, c.last_update, c.active - Hash (cost=67.03..67.03 rows=603 width=695) (actual time=1.806..1.809 rows=603 loops=1) Output: a.address, a.district, a.phone, a.address_id Buckets: 1024 Batches: 1 Memory Usage: 440kB - Seq Scan on public.address_pgp_pub a (cost=0.00..67.03 rows=603 width=695) (actual time=0.004..0.556 rows=603 loops=1) Output: a.address, a.district, a.phone, a.address_id Planning Time: 0.536 ms Execution Time: 11499.672 ms (13 rows) Time: 11501.474 ms (00:11.501)
Interprétation des résultats.
Une fois ces tests effectués, nous pouvons en tirer des conclusions.
stockage
Concernant l’espace disque de chacune de ces tables, nous voyons de grandes différences
(postgres@[local]:5433) [dvdrental] select tablename , pg_size_pretty(pg_table_size(tablename::varchar)) as SizeMo from pg_tables where tablename like 'custo%' or tablename like 'addre%' order by tablename,2; tablename | sizemo ----------------------+-------- address | 88 kB address_encrypt | 104 kB address_pgp_pub | 520 kB address_pgp_sym_256 | 224 kB customer | 96 kB customer_encrypt | 120 kB customer_pgp_pub | 720 kB customer_pgp_sym_256 | 288 kB (10 rows)
-> Le chiffrement simple demande donc 20% de stockage en plus par rapport aux tables d’origine.
-> Avec le chiffrement symétrique et compression algo aes256, cela double le volume disque vis-à-vis du chiffrement classique.
-> Enfin le chiffrement avec clé publique nécessite des tables 5 à 6 fois plus volumineuses que celles d’origine.
Les temps d’exécution
Comme nous avons pu le constater au cours de ce test, le chiffrement engendre des temps de réponses bien plus importants sur notre requête test.
-> Pour le chiffrement simple, un temps d’exécution en insertion et sélection quasi identique. Mais l’échantillon de données n’est pas forcément le plus représentatif car celui-ci est plutôt restreint.
-> Pour le chiffrement symétrique avec compression algo aes256, temps d’exécution de l’insertion et de la sélection beaucoup plus longs que le chiffrement classique. Nous passons de 5 millisecondes au SELECT et INSERT à plus de 1000 millisecondes pour l’INSERT et plus de 2800 millisecondes pour le SELECT.
-> Pour le chiffrement avec clé publique/clé privée, à l’insertion, c’est plutôt rapide (quelques centaines de millisecondes).
Au select, nous dépassons les 10 secondes, pour une requête qui met 5 millisecondes sans aucun chiffrage.
Nous devons prendre en compte le fait que pour notre test, nous travaillons sur une VM extrêmement sous dimensionnée (ec2 type t2 micro).
Cependant, nous n’avons sélectionné que quelques centaines de lignes dans nos tables, ce qui représente un échantillon faible.
Qu’en serait-il sur des tables de plus 100 millions de lignes ? La manipulation de données chiffrées volumineuses exigerait, sans aucun doute, plus de ressources en terme de CPU et RAM.
🙂
Emmanuel Rami
Continuez votre lecture sur le blog :
- “Pruning” de partitions sous PostgreSQL ou comment bien élaguer ! (Capdata team) [PostgreSQL]
- PostgreSQL 13 : présentation (Emmanuel RAMI) [PostgreSQL]
- Quelles solutions de chiffrement de données pour MySQL / MariaDB (David Baffaleuf) [MySQL]
- Le chiffrement et SQL Server – Episode 2 : Mise en oeuvre de TDE (Capdata team) [SQL Server]
- Le chiffrement et SQL Server – Episode 1 : Transparent Data Encryption (TDE) vs Always Encrypted (Capdata team) [SQL Server]
Cryptage nooooooooooooon
https://chiffrer.info/
C’est très intéressant comme article. Chiffrement, c’est mieux à lire que Cryptage, trois d’affilé sur la fin. Un petit tableau final avec les temps pour comparer visuellement comme pour le stockage ?
Avec une colonne prénom, je me demandais sachant que certains prénom sont sur d’être dans la table genre Patricia par exemple (selon le pays), je me demandais si en faisant l’hypothèse que deux ou trois prénoms étaient dans la colonne, s’il n’était pas alors plus facile de trouver la clef, sachant que la même clef chiffre tous les champs ? Bref, question de néophytes en chiffrement.
Bonjour
vous avez bien raison, les anglicismes, il y’en bien de trop dans le monde informatique….
C’est corrigé !
Pour le fait de retrouver 2 champs avec même valeur de chiffrement si le terme est identique, je ne suis pas sur que cela puisse se vérifier. Je vais tester cela.
Merci pour votre lecture !