Chiffre César en C: didacticiel de programmation étape par étape
Apprenez à implémenter le chiffre César en C avec des exemples de code étape par étape. Couvre les fonctions de base, les approches basées sur des pointeurs, la gestion ASCII, le cryptage de fichiers, les arguments de ligne de commande et la sécurité de la mémoire.
Le chiffre César est un premier projet de cryptographie naturel pour les programmeurs C. L'implémenter en C vous donne une exposition directe au codage de caractères, à l'arithmétique des pointeurs, à la gestion de la mémoire et aux E/S de fichiers, autant de compétences de base qui distinguent les développeurs C compétents. Contrairement aux langages de niveau supérieur qui font abstraction de ces détails, le C vous oblige à réfléchir attentivement à la manière dont les caractères sont stockés et manipulés au niveau des octets.
Ce didacticiel présente cinq implémentations de plus en plus avancées: une fonction de base utilisant l'indexation de tableaux, une approche basée sur un pointeur, un programme qui gère correctement la table ASCII complète, un utilitaire de chiffrement de fichiers et un outil de ligne de commande raffiné avec une analyse appropriée des arguments. Tout le code est compilé avec GCC ou Clang en utilisant la norme C11 ou version ultérieure.
Essayez-le en ligne: expérimentez le cryptage du chiffre César avant d'écrire du code à l'aide de notre encodeur du chiffre César gratuit.
Comment fonctionne le chiffre César avec ASCII
En C, les caractères sont stockés sous forme de valeurs entières selon la norme ASCII. Les lettres majuscules A à Z occupent les valeurs 65 à 90 et les lettres minuscules a à z occupent les valeurs 97 à 122. Ces plages sont contiguës, ce qui rend l'arithmétique de décalage simple.
La formule de cryptage pour un seul caractère est:
encrypted = (ch - base + shift) % 26 + base
où base est 'A' (65) pour les majuscules ou 'a' (97) pour les minuscules, et shift est une valeur comprise entre 0 et 25. L'opération modulo garantit que le résultat revient de Z à A.
Un détail important en C: l'opérateur % peut renvoyer des valeurs négatives lorsque l'opérande de gauche est négatif. Par exemple, (-3) % 26 donne -3 en C, et non 23. Lors de la mise en œuvre du décryptage ou de la gestion des décalages négatifs, vous devez tenir compte de ce comportement. La solution standard consiste à en ajouter 26 avant de prendre le modulo: ((ch - base - shift) % 26 + 26) % 26 + base.
Implémentation de base du chiffre César
L'implémentation la plus simple utilise deux fonctions qui opèrent sur des tableaux de caractères. Cette version traite les chaînes sur place, modifiant le tampon d'origine.
#include <stdio.h>
#include <string.h>
#include <ctype.h>
void caesar_encrypt(char *text, int shift) {
/* Normalize shift to 0-25 range */
shift = ((shift % 26) + 26) % 26;
for (int i = 0; text[i] != '\0'; i++) {
if (isupper((unsigned char)text[i])) {
text[i] = (text[i] - 'A' + shift) % 26 + 'A';
} else if (islower((unsigned char)text[i])) {
text[i] = (text[i] - 'a' + shift) % 26 + 'a';
}
/* Non-alphabetic characters remain unchanged */
}
}
void caesar_decrypt(char *text, int shift) {
/* Decryption is encryption with the complementary shift */
caesar_encrypt(text, 26 - ((shift % 26 + 26) % 26));
}
int main(void) {
char message[] = "Hello, World! The quick brown fox jumps over the lazy dog.";
int shift = 7;
printf("Original: %s\n", message);
caesar_encrypt(message, shift);
printf("Encrypted: %s\n", message);
caesar_decrypt(message, shift);
printf("Decrypted: %s\n", message);
return 0;
}
Compilez et exécutez:
gcc -std=c11 -Wall -Wextra -o caesar caesar_basic.c
./caesar
Sortie:
Original: Hello, World! The quick brown fox jumps over the lazy dog.
Encrypted: Olssv, Dvysk! Aol xbpjr iyvdu mve qbtwz vcly aol shgf kvn.
Decrypted: Hello, World! The quick brown fox jumps over the lazy dog.
Plusieurs détails spécifiques au C méritent attention:
- Casting
unsigned char: les fonctionsisupper()etislower()attendent une valeurunsigned charouEOF. Passer un simplecharsur les plates-formes oùcharest signé peut provoquer un comportement indéfini pour les valeurs supérieures à 127. Toujours convertir enunsigned charlors du passage de caractères aux fonctions<ctype.h>. - Modification sur place: La fonction modifie directement la chaîne d'entrée. Ceci est efficace (aucune allocation de mémoire nécessaire) mais signifie que vous devez transmettre un tableau de caractères mutable, pas une chaîne littérale. L'écriture de
char *msg = "Hello"; caesar_encrypt(msg, 3);serait un comportement indéfini car les littéraux de chaîne sont en lecture seule. - Terminateur nul: la boucle utilise
text[i]!= '\0'comme condition de terminaison, qui est l'idiome C standard pour itérer sur des chaînes terminées par un caractère nul.
Implémentation basée sur des pointeurs
Les programmeurs C préfèrent souvent l’arithmétique des pointeurs à l’indexation des tableaux. La version suivante utilise partout des pointeurs, que de nombreux développeurs C expérimentés considèrent comme plus idiomatiques. Cette version renvoie également une chaîne nouvellement allouée plutôt que de modifier l'entrée.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
char *caesar_transform(const char *text, int shift) {
shift = ((shift % 26) + 26) % 26;
size_t len = strlen(text);
char *result = malloc(len + 1);
if (result == NULL) {
return NULL; /* Allocation failed */
}
const char *src = text;
char *dst = result;
while (*src != '\0') {
if (*src >= 'A' && *src <= 'Z') {
*dst = (*src - 'A' + shift) % 26 + 'A';
} else if (*src >= 'a' && *src <= 'z') {
*dst = (*src - 'a' + shift) % 26 + 'a';
} else {
*dst = *src;
}
src++;
dst++;
}
*dst = '\0';
return result;
}
char *caesar_encrypt_alloc(const char *text, int shift) {
return caesar_transform(text, shift);
}
char *caesar_decrypt_alloc(const char *text, int shift) {
return caesar_transform(text, 26 - ((shift % 26 + 26) % 26));
}
int main(void) {
const char *original = "Secrets must be kept safe!";
int shift = 13;
char *encrypted = caesar_encrypt_alloc(original, shift);
if (encrypted == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return 1;
}
char *decrypted = caesar_decrypt_alloc(encrypted, shift);
if (decrypted == NULL) {
fprintf(stderr, "Memory allocation failed\n");
free(encrypted);
return 1;
}
printf("Original: %s\n", original);
printf("Encrypted: %s\n", encrypted);
printf("Decrypted: %s\n", decrypted);
/* Always free allocated memory */
free(encrypted);
free(decrypted);
return 0;
}
Cette approche présente plusieurs avantages:
- Correction de la const: la chaîne d'entrée est déclarée
const char *, de sorte que le compilateur détectera toute tentative de modification accidentelle. - Aucun effet secondaire: La chaîne d'origine n'est jamais modifiée, ce qui rend la fonction plus sûre à utiliser dans du code multithread ou lorsque l'entrée doit être préservée.
- Gestion explicite de la mémoire: l'appelant est responsable de la libération de la chaîne renvoyée, qui est le modèle C standard pour les fonctions qui allouent de la mémoire.
Le modèle d'incrémentation du pointeur (src++; dst++;) traite un caractère à la fois, avançant à la fois les positions de lecture et d'écriture. Ceci est légèrement plus efficace que l'indexation de tableaux car cela évite les calculs de décalage répétés, bien que les compilateurs modernes optimisent généralement les deux formulaires pour un code machine identique.
Programme de cryptage de fichiers
Un programme de chiffrement César pratique devrait être capable de crypter et de décrypter des fichiers. L'implémentation suivante lit un fichier en morceaux mis en mémoire tampon, transforme chaque caractère et écrit le résultat dans un fichier de sortie. Cette approche gère les fichiers de n'importe quelle taille sans charger l'intégralité du contenu en mémoire.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#define BUFFER_SIZE 4096
static int normalize_shift(int shift) {
return ((shift % 26) + 26) % 26;
}
static char transform_char(char ch, int shift) {
if (ch >= 'A' && ch <= 'Z') {
return (ch - 'A' + shift) % 26 + 'A';
} else if (ch >= 'a' && ch <= 'z') {
return (ch - 'a' + shift) % 26 + 'a';
}
return ch;
}
int caesar_process_file(const char *input_path, const char *output_path,
int shift, int decrypt) {
FILE *fin = fopen(input_path, "r");
if (fin == NULL) {
fprintf(stderr, "Error opening input file '%s': %s\n",
input_path, strerror(errno));
return -1;
}
FILE *fout = fopen(output_path, "w");
if (fout == NULL) {
fprintf(stderr, "Error opening output file '%s': %s\n",
output_path, strerror(errno));
fclose(fin);
return -1;
}
shift = normalize_shift(shift);
if (decrypt) {
shift = 26 - shift;
}
char buffer[BUFFER_SIZE];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, fin)) > 0) {
for (size_t i = 0; i < bytes_read; i++) {
buffer[i] = transform_char(buffer[i], shift);
}
size_t bytes_written = fwrite(buffer, 1, bytes_read, fout);
if (bytes_written != bytes_read) {
fprintf(stderr, "Error writing to output file: %s\n",
strerror(errno));
fclose(fin);
fclose(fout);
return -1;
}
}
if (ferror(fin)) {
fprintf(stderr, "Error reading input file: %s\n", strerror(errno));
fclose(fin);
fclose(fout);
return -1;
}
fclose(fin);
fclose(fout);
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 5) {
fprintf(stderr, "Usage: %s <encrypt|decrypt> <shift> <input_file> <output_file>\n",
argv[0]);
return 1;
}
int decrypt = 0;
if (strcmp(argv[1], "decrypt") == 0 || strcmp(argv[1], "d") == 0) {
decrypt = 1;
} else if (strcmp(argv[1], "encrypt") != 0 && strcmp(argv[1], "e") != 0) {
fprintf(stderr, "Invalid mode '%s'. Use 'encrypt' or 'decrypt'.\n", argv[1]);
return 1;
}
char *endptr;
long shift = strtol(argv[2], &endptr, 10);
if (*endptr != '\0') {
fprintf(stderr, "Invalid shift value '%s'. Must be an integer.\n", argv[2]);
return 1;
}
int result = caesar_process_file(argv[3], argv[4], (int)shift, decrypt);
if (result == 0) {
printf("File %s successfully: %s -> %s\n",
decrypt ? "decrypted" : "encrypted",
argv[3], argv[4]);
}
return result == 0 ? 0 : 1;
}
Compiler et utiliser:
gcc -std=c11 -Wall -Wextra -o caesar_file caesar_file.c
# Encrypt a file
./caesar_file encrypt 5 plaintext.txt encrypted.txt
# Decrypt it back
./caesar_file decrypt 5 encrypted.txt decrypted.txt
Décisions de conception clés dans cette implémentation:
- E/S tamponnées: la lecture de 4 096 octets à la fois est beaucoup plus efficace que les E/S caractère par caractère. La taille du tampon correspond à une taille de bloc typique du système de fichiers.
- Gestion des erreurs: chaque opération sur les fichiers recherche les erreurs et les messages d'erreur incluent l'erreur système spécifique via
strerror(errno). - Nettoyage des ressources: les fichiers sont fermés sur tous les chemins de code, y compris les chemins d'erreur. Dans le code de production, vous pouvez utiliser le modèle
goto cleanuppour une gestion des ressources encore plus propre. - Validation d'entrée: la valeur de décalage est analysée avec
strtol()et vérifiée pour les caractères indésirables de fin, ce qui est plus robuste queatoi().
Outil de ligne de commande avec Brute Force
Le dernier exemple est un outil de ligne de commande complet qui prend en charge le cryptage, le déchiffrement, le craquage par force brute et la lecture à partir d'une entrée standard. Il démontre une analyse correcte des arguments C et l'approche par force brute pour briser le chiffre de César.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAX_INPUT 65536
static int normalize_shift(int shift) {
return ((shift % 26) + 26) % 26;
}
static void caesar_transform_inplace(char *text, int shift) {
shift = normalize_shift(shift);
for (char *p = text; *p != '\0'; p++) {
if (*p >= 'A' && *p <= 'Z') {
*p = (*p - 'A' + shift) % 26 + 'A';
} else if (*p >= 'a' && *p <= 'z') {
*p = (*p - 'a' + shift) % 26 + 'a';
}
}
}
static char *read_stdin(void) {
char *buffer = malloc(MAX_INPUT);
if (buffer == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
size_t total = 0;
size_t bytes_read;
while (total < MAX_INPUT - 1 &&
(bytes_read = fread(buffer + total, 1, MAX_INPUT - 1 - total, stdin)) > 0) {
total += bytes_read;
}
buffer[total] = '\0';
return buffer;
}
static void brute_force(const char *ciphertext) {
size_t len = strlen(ciphertext);
char *attempt = malloc(len + 1);
if (attempt == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
printf("=== Brute Force: All 26 Possible Decryptions ===\n\n");
for (int shift = 0; shift < 26; shift++) {
strcpy(attempt, ciphertext);
caesar_transform_inplace(attempt, 26 - shift);
printf("Shift %2d: %s\n", shift, attempt);
}
free(attempt);
}
static void print_usage(const char *progname) {
printf("Caesar Cipher Tool\n\n"
"Usage:\n"
" %s encrypt <shift> <text>\n"
" %s decrypt <shift> <text>\n"
" %s bruteforce <text>\n"
" echo \"text\" | %s encrypt <shift>\n"
"\nExamples:\n"
" %s encrypt 3 \"Hello World\"\n"
" %s decrypt 3 \"Khoor Zruog\"\n"
" %s bruteforce \"Khoor Zruog\"\n",
progname, progname, progname, progname,
progname, progname, progname);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
print_usage(argv[0]);
return 0;
}
if (strcmp(argv[1], "encrypt") == 0 || strcmp(argv[1], "decrypt") == 0) {
if (argc < 3) {
fprintf(stderr, "Error: shift value required\n");
return 1;
}
char *endptr;
int shift = (int)strtol(argv[2], &endptr, 10);
if (*endptr != '\0') {
fprintf(stderr, "Error: invalid shift value '%s'\n", argv[2]);
return 1;
}
int is_decrypt = (strcmp(argv[1], "decrypt") == 0);
if (is_decrypt) {
shift = 26 - normalize_shift(shift);
}
char *text;
int free_text = 0;
if (argc >= 4) {
/* Text provided as argument */
text = argv[3];
} else {
/* Read from stdin */
text = read_stdin();
if (text == NULL) return 1;
free_text = 1;
}
/* Work on a mutable copy if text is from argv */
char *work;
if (!free_text) {
work = malloc(strlen(text) + 1);
if (work == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return 1;
}
strcpy(work, text);
} else {
work = text;
}
caesar_transform_inplace(work, shift);
printf("%s\n", work);
if (!free_text) {
free(work);
} else {
free(text);
}
} else if (strcmp(argv[1], "bruteforce") == 0) {
char *text;
int free_text = 0;
if (argc >= 3) {
text = argv[2];
} else {
text = read_stdin();
if (text == NULL) return 1;
free_text = 1;
}
brute_force(text);
if (free_text) free(text);
} else if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
print_usage(argv[0]);
} else {
fprintf(stderr, "Unknown command '%s'\n", argv[1]);
print_usage(argv[0]);
return 1;
}
return 0;
}
Compiler et utiliser:
gcc -std=c11 -Wall -Wextra -O2 -o caesar_tool caesar_tool.c
# Encrypt
./caesar_tool encrypt 13 "Meet me at midnight"
# Decrypt
./caesar_tool decrypt 13 "Zrrg zr ng zvqavtug"
# Brute force an unknown message
./caesar_tool bruteforce "Wklv lv d vhfuhw phvvdjh"
# Pipe from another command
echo "Secret message" | ./caesar_tool encrypt 5
Meilleures pratiques en matière de sécurité de la mémoire
La sécurité de la mémoire est la préoccupation la plus critique lors de l’écriture de code C. Voici les pratiques démontrées tout au long de ce didacticiel et des recommandations supplémentaires:
Toujours vérifier les valeurs de retour malloc: chaque appel à malloc() peut renvoyer NULL si le système manque de mémoire. Le déréférencement d’un pointeur nul est un comportement indéfini qui provoque généralement une erreur de segmentation.
Libérez ce que vous allouez: chaque malloc() doit avoir un free() correspondant. Utilisez des outils comme Valgrind pour vérifier que votre programme ne présente aucune fuite de mémoire:
valgrind --leak-check=full ./caesar_tool encrypt 5 "Hello"
Utilisez strtol au lieu de atoi: La fonction atoi() ne fournit aucune détection d'erreur. Si un utilisateur transmet "abc" comme valeur de décalage, atoi() renvoie silencieusement 0. La fonction strtol() vous permet de vérifier si la chaîne entière a été consommée via le paramètre endptr.
Prévention du débordement de tampon: lors de la lecture d'une entrée, appliquez toujours une taille maximale. La fonction read_stdin() dans le dernier exemple limite la saisie aux octets MAX_INPUT et garantit une terminaison nulle.
Correction de la const: marquez les paramètres du pointeur comme const lorsque la fonction ne doit pas modifier les données vers lesquelles ils pointent. Cela détecte les bogues au moment de la compilation et documente le contrat de la fonction.
Indicateurs du compilateur pour un code plus sûr
Compilez toujours avec les avertissements activés. Les indicateurs suivants détectent de nombreuses erreurs courantes au moment de la compilation:
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -O2 -o caesar caesar.c
-Wallactive les avertissements les plus courants-Wextraactive des avertissements supplémentaires non couverts par-Wall-Wpedanticimpose une stricte conformité ISO C-Werrortraite tous les avertissements comme des erreurs, vous obligeant à les corriger-O2permet l'optimisation, qui permet également une analyse supplémentaire pouvant détecter les bogues
Pour le développement, pensez également à -fsanitize=address,undefined qui permet de vérifier à l'exécution les erreurs de mémoire et les comportements non définis:
gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined -g -o caesar caesar.c
Erreurs courantes à éviter
Modification des littéraux de chaîne: en C, les littéraux de chaîne comme "Hello" sont stockés dans la mémoire en lecture seule. Les attribuer à un char * (et non à const char *) puis les modifier est un comportement indéfini. Utilisez toujours des tableaux de caractères (char msg[] = "Hello";) lorsque vous avez besoin d'une chaîne mutable.
Pièges des caractères signés: sur de nombreuses plates-formes, char est signé par défaut, ce qui signifie que les valeurs supérieures à 127 sont négatives. Les transmettre aux fonctions <ctype.h> est un comportement indéfini. Diffusez d'abord sur unsigned char.
Un par un dans l'allocation: lors de l'allocation d'espace pour une chaîne, ajoutez toujours 1 pour le terminateur nul. malloc(strlen(text)) est faux; malloc(strlen(text) + 1) est correct.
Oublier le terminateur nul: après avoir copié ou construit une chaîne dans un tampon, assurez-vous toujours que le dernier octet est '\0'. Des fonctions telles que strncpy() ne garantissent pas la terminaison nulle lorsque la chaîne source est plus longue que le nombre spécifié.
Résumé
Ces cinq implémentations montrent comment créer des programmes de chiffrement César en C, depuis des fonctions simples jusqu'à des outils de ligne de commande de qualité production. L'implémentation de base montre la manipulation des chaînes de base. La version pointeur illustre le style de codage C idiomatique. Le chiffreur de fichiers gère les E/S du monde réel avec une gestion appropriée des erreurs. Et l'outil CLI rassemble tout avec des capacités d'analyse d'arguments et de force brute.
La nature bas niveau du C en fait un excellent langage pour comprendre exactement comment fonctionnent les opérations de chiffrement au niveau des caractères. La discipline de gestion de la mémoire requise par le C vous prépare également à la mise en œuvre de chiffrements plus complexes où les structures de données et la gestion des tampons deviennent plus difficiles.
Prochaines étapes: Explorez notre Décodeur de chiffre César pour voir comment fonctionne le décryptage automatisé, ou découvrez le Chiffre de Vigenere pour une approche polyalphabétique plus sécurisée.