このページでは、「シーザー暗号」による文字列の暗号化と復号化をC言語でプログラミングしていきたいと思います。
シーザー暗号と聞くとなんだか難しそうですが、実は非常に簡単な考え方の暗号になります。
解読も簡単なので実用性はほぼないですが、暗号を初めて学ぶ人にもおすすめの題材だと思います。また、特にC言語入門者の方にとっては、”文字の扱い” を復習するのにもよい題材になると思います。
シーザー暗号とは
まずシーザー暗号がどのような暗号であるかについて解説しておきます。
すぐにシーザー暗号のプログラムのソースコードを読みたい方はシーザー暗号のプログラム例までスキップしていただければと思います。
シーザー暗号とは、平文の各文字をアルファベット順的に特定の量だけシフトしたものを暗号文とする暗号になります。
暗号化前の誰にでも読むことができるデータを平文と呼びます
一方で、その平文を何らかの手法で暗号化したデータを暗号文と呼びます
ここで、具体的に平文の各文字をアルファベット順的に “3
文字分” “左に” ずらすことで暗号化することを考えてみましょう。
例えば、平文の中の文字 D
を 3
文字分左にシフトすれば A
に変化します。また、E
を 3
文字分左にシフトすれば B
に変化します。さらに、A
を左に 3
文字分シフトすれば、一周回って X
に変化します。
同様に各文字をアルファベット順的に左に 3
文字分シフトした場合の対応を示したものが下記のようになります。
シフト前:ABCDEFGHIJKLMNOPQRSTUVWXYZ シフト後:XYZABCDEFGHIJKLMNOPQRSTUVW
このシフトにより元の文字から違う文字に変換され、普通には読むことのできない文字列が出来上がります。
暗号化
この考えに基づいて、下記の文字列を平文としてシーザー暗号に変換してみましょう。
MY NAME IS YAMADA TARO.
シフトする量はここまでの説明と同様に、左に 3
文字としたいと思います。
1文字目の M
を左に 3
文字分シフトすると J
に、2文字目の Y
を左に 3
文字分シフトすると V
に変換されます。
こんな感じで各文字をアルファベット順的に左に 3
文字分シフトしていきます。
全文字をシフトすれば、下記のような暗号文が出来上がります(今回はスペースやピリオドは変換せずに暗号化後も同じ文字であるとしたいと思います)。
JV KXJB FP VXJXAX QXOL.
どうでしょう?すごく簡単な処理で暗号化をしたにも関わらず、何が書かれているか意味不明な暗号文が作成できたことを確認できると思います。
こんな感じで、平文の各文字をアルファベット順的にシフトすることで暗号化を行うのがシーザー暗号になります。
今回はシフトを左方向に 3
文字分と行っていますが、もちろん左方向にシフトしても良いですし、他の量をシフトするようにしても良いです。
スポンサーリンク
スポンサーリンク
復号化
では次に、シーザー暗号で暗号化された暗号文を平文に戻す復号化について考えてみましょう。
例えばあなたは下記の文章を受け取ったとします。さて、この文章をあなたは解読できるでしょうか?
TJPM KVNNRJMY DN VDPZJ.
おそらくパッと見では意味不明だと思います。
ですが、”シーザー暗号で暗号化されている” ことと、”暗号化時に左に 5
文字分シフトされた” ということさえ分かれば簡単に解読することが出来ると思います。
暗号化時に左に 5
文字分シフトされたということは、暗号化後の文字を、左の逆方向である右方向に 5
文字分シフトすれば暗号化前の文字に戻るということになります。
1文字目の T
を右に 5
文字分シフトすれば Y
になりますし、2文字目の J
を右に 5
文字文シフトすれば O
になります。
同様にして、全文字を右に 5
文字分シフトした結果は下記のようになります。
YOUR PASSWORD IS AIUEO.
意味不明だった暗号文が、ちゃんと意味の分かる文章に戻せたことが確認できると思います。
こんな感じでシーザー暗号の暗号文は、暗号化時のシフトを打ち消す形で各文字をシフト(つまり逆方向に各文字をシフト)することで復号することができます。
このシーザー暗号は、”考え方的には” 暗号化時にシフトした量が分からなければ復号することはできません。
ですので、内容を理解して欲しい相手にだけシフト量を伝えるようにすれば、もし他の人に暗号が読まれても内容が悟られないようにすることができます。
このシフト量のような、暗号を解くためのヒントを「鍵」と言ったりします。この「鍵」はシーザー暗号以外の暗号においても利用される考え方なので覚えておくと良いと思います。
まあ、ただ、このあたりの話はあくまでも考え方の話であって、シフト量に設定できる値は有限なので、実際にはシフト量に対して総当たりで復号を試してやることで復号化を行うことができてしまいます。
つまり、簡単に第三者から復号ができてしまいます。そのため、このページの冒頭でも述べたように実用性は低い暗号になります。
シーザー暗号の実装方法
続いては、ここまで解説してきたシーザー暗号の暗号化と復号化の実装について考えていきたいと思います。
おそらくここまでの解説でどのように実装すれば良いか予想がついてる人も多いと思いますので、そういった方は是非自身でプログラミングしてみてください。
暗号化
前述の通り、シーザー暗号では平文の各文字を特定の量だけシフトすることで暗号化することができます。
これを実装する上でポイントになるのは “文字のシフト” だと思います。ある文字を特例の量シフトした時に何の文字になるのかを求める必要があります。
ただ、これはC言語では簡単に求めることができます。ご存知の通り(?)、C言語で扱う文字は全て “数値” です。
その証拠に char
型の変数に文字を格納して下記のように printf
で表示すると、数値が表示されます(下記の場合は 88
が表示されます)。
char moji;
moji = 'X';
printf("%d\n", moji);
文字列も、要は文字の配列(char
型の配列)なので、同様に各要素は数値として扱われていると考えることができます。
ただし、上記の printf
に指定している %d
を %c
に置き換えるだけで、文字を表示するようなこともできます。ただ、これは数値を、その数値に割り当てられた文字として表示しているだけです。
これは、下記のようにprintf
を実行することからも確認することが出来ると思います。この場合は %c
を指定しているので文字が表示されることになります。数値 75
が割り当てられている文字は 'K'
なので、下記を実行すれば 'K'
が表示されます。
printf("%c\n", 75);
要は、各文字にはそれぞれ数値が割り当てられていて、変数に格納する際には、その文字ではなく数値が格納されることになります(この文字に割り当てられる数値を文字コードと言います)。
なので、文字を格納した変数においても “足し算や引き算などの演算を実行することが可能” です。
また、各文字にどんな数値が割り振られているかは、下記のように文字を printf
に %d
を指定して表示してやれば確認することができます。
printf("%d\n", 'A');
例えば大文字のアルファベットに対しては、'A'
には 65
、'B'
には 66
、'C'
には 67
、…、'Z'
には 90
といったように、65
から 90
までの数値が昇順に割り振られています。
このように昇順に割り振られているので、文字をアルファベット順に右方向にシフトする際には、そのシフト量分の足し算を実行してやれば良いことになります。逆に左方向にシフトする際には引き算を実行してやれば良いです。
例えば平文の文字列 plain
の i
文字目を左方向に shift
文字分シフトした結果を暗号文 cipher
の i
文字目とする場合は、下記のように引き算を行えば良いことになります。
cipher[i] = plain[i] - shift;
つまり、上記により平文の1文字分が暗号に変換されたことになります。ですので、これを全文字に対してループ処理などで実行してやれば、平文全文字をシーザー暗号に暗号化できます(その結果が cipher
となる)。
ただし、単に上記のように引き算を行なってしまうと plain[i]
が大文字アルファベット('A'
〜 'Z'
)であったとしても、引き算結果が結果が 'A'
を表す数値 65
よりも小さくなってしまう場合があり、この場合は大文字アルファベット以外の変換されることになります。
これが嫌な場合は、引き算結果を 'A'
〜 'Z'
に割り当てられた数値の範囲に丸め込むような処理を行う必要があります。この丸め込む処理の例は下記のようになります。
/* 文字が'A'から何文字目の文字であるかを計算 */
numFromA = plain[i] - 'A';
/* シフト */
shiftNumFromA = numFromA - shift;
/* shifNumFromAが負の値の場合は正の値に変換 */
while (shiftNumFromA < 0) {
/* 後で剰余算を行う数を足す */
shiftNumFromA += alphaLen;
}
/* アルファベット数以内に収める */
shiftNumFromA = shiftNumFromA % alphaLen;
/* 'A'からshiftNumFromA文字目の文字を暗号化結果とする */
cipher[i] = 'A' + shiftNumFromA;
while
ループでは、その次に行う剰余算を正の値同士で実行できるように shiftNumFromA
が正の値になるまで繰り返し足し算を行なっています(負の値の剰余算がややこしいので)。
スポンサーリンク
スポンサーリンク
復号化
復号を行う上で重要なのは、暗号化で行なった処理が “元に戻るように” 処理を行うことです。
先程紹介した暗号化は文字を左方向にシフトすることで実施しましたので、文字を右方向にシフトすることで文字を元通りにする(復号する)ことが出来ます。これは、シフトを引き算ではなく “足し算” で実行することで実現することができます。
例えば cipher
という文字列の i
文字目を右方向に shift
文字分シフトした結果を plain
という文字列の i
文字目とする場合は、下記のように引き算してやれば良いことになります。
plain[i] = cipher[i] + shift;
これを cipher
の全文字に対して実行することで、シフト量 shift
で暗号化された暗号文 cipher
を平文に変換した文字列 plain
が得られます。
ただし、当然ですが暗号化した時のシフト量と “異なる値” でシフトしてしまうと上手く復号できないので注意してください。
また、暗号化の時と同様、上記の足し算の結果は 'A'
〜 'Z'
に割り当てられた数値の範囲に収まるとは限りません。もし暗号化時に引き算結果を 'A'
〜 'Z'
に収まるように丸め込めの処理を行なっている場合、この復号化時にも同様に丸め込めの処理を行う必要があるので注意してください。
シーザー暗号のプログラム例
最後にここまで解説してきた内容を踏まえて作成した、シーザー暗号の暗号化プログラムと復号化プログラムの例を紹介していきたいと思います。
暗号化プログラム
下記が文字列をシーザー暗号に暗号化するプログラムのソースコードになります。
encrypto
が実際に暗号化を行う関数です。何を行なっているかはシーザー暗号の実装方法を参照していただければと思います。
暗号化する文字はアルファベットのみです。アルファベット以外の文字はそのまま出力されます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 入力可能な平文の文字列長 */
#define MAX_STR 255
/** encrypto
* 文字列をシーザー暗号に暗号化する
* cipher:暗号化後の文字列(出力)
* plain:平文の文字列(入力)
* shift:シフト量(入力)
*/
void encrypto(char *cipher, char *plain, int shift) {
int i;
int alphaLen;
int numFromA;
int shiftNumFromA;
/* アルファベットの文字数を計算 */
alphaLen = 'Z' - 'A' + 1;
for (i = 0; i < strlen(plain); i++) {
if (plain[i] >= 'A' && plain[i] <= 'Z') {
/* 大文字アルファベットの場合 */
/* 文字が'A'から何文字目の文字であるかを計算 */
numFromA = plain[i] - 'A';
/* シフト */
shiftNumFromA = numFromA - shift;
/* shifNumFromAが負の値の場合は正の値に変換 */
while (shiftNumFromA < 0) {
/* 後で剰余算を行う数を足す */
shiftNumFromA += alphaLen;
}
/* アルファベット数以内に収める */
shiftNumFromA = shiftNumFromA % alphaLen;
/* 'A'からshiftNumFromA文字目の文字を暗号化結果とする */
cipher[i] = 'A' + shiftNumFromA;
} else if (plain[i] >= 'a' && plain[i] <= 'z') {
/* 小文字アルファベットの場合 */
/* 文字が'a'から何文字目の文字であるかを計算 */
numFromA = plain[i] - 'a';
/* シフト */
shiftNumFromA = numFromA - shift;
/* shifNumFromAが負の値の場合は正の値に変換 */
while (shiftNumFromA < 0) {
/* 後で剰余算を行う数を足す */
shiftNumFromA += alphaLen;
}
/* アルファベット数以内に収める */
shiftNumFromA = shiftNumFromA % alphaLen;
/* 'a'からshiftNumFromA文字目の文字を暗号化結果とする */
cipher[i] = 'a' + shiftNumFromA;
}else {
/* アルファベット以外は変換しない */
cipher[i] = plain[i];
}
}
/* 最後の文字の後ろにヌル文字を付加する */
cipher[strlen(plain)] = '\0';
}
int main(int argc, char *argv[]) {
char plain[MAX_STR + 1];
char cipher[MAX_STR + 1];
if (argc != 3) {
printf("引数に平文とシフト量を指定してください\n");
return -1;
}
if (strlen(argv[1]) > MAX_STR) {
printf("平文が長すぎます\n");
return -1;
}
strcpy(plain, argv[1]);
printf("平文:%s\n", plain);
encrypto(cipher, plain, atoi(argv[2]));
printf("暗号:%s\n", cipher);
return 0;
}
実行する際には、下記のように第1引数に平文、第2引数にシフト量を指定して実行します。実行すると平文をシーザー暗号で暗号化した結果が表示されます。
$ ./encrypto.exe "My Name Is Yamada Taro." 5 平文:My Name Is Yamada Taro. 暗号:Ht Ivhz Dn Tvhvyv Ovmj.
平文は1つの引数に収まるように "
で括って実行してください(スペースなどが含まれると別の引数として扱われてしまうため)。
スポンサーリンク
復号化プログラム
下記がシーザー暗号を復号化するプログラムのソースコードになります。
ただし復号できるのは、アルファベットのみを暗号化したシーザー暗号のみです。
decrypto
が実際に復号化を行う関数です。基本的に先ほど紹介した encrypto
関数と同じで、違いはシフトを足し算ではなく引き算で行っている点のみです。
復号化する文字はアルファベットのみです。アルファベット以外の文字はそのまま出力されます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 入力可能な平文の文字列長 */
#define MAX_STR 255
/** decrypto
* シーザー暗号を復号化する
* plain:cipherの復号化結果(出力)
* cipher:シーザー暗号の文字列(入力)
* shift:シフト量(入力)
*/
void decrypto(char *plain, char *cipher, int shift) {
int i;
int alphaLen;
int numFromA;
int shiftNumFromA;
/* アルファベットの文字数を計算 */
alphaLen = 'Z' - 'A' + 1;
for (i = 0; i < strlen(cipher); i++) {
if (cipher[i] >= 'A' && cipher[i] <= 'Z') {
/* 大文字アルファベットの場合 */
/* 文字が'A'から何文字目の文字であるかを計算 */
numFromA = cipher[i] - 'A';
/* シフト */
shiftNumFromA = numFromA + shift;
/* shifNumFromAが負の値の場合は正の値に変換 */
while (shiftNumFromA < 0) {
/* 後で剰余算を行う数を足す */
shiftNumFromA += alphaLen;
}
/* アルファベット数以内に収める */
shiftNumFromA = shiftNumFromA % alphaLen;
/* 'A'からshiftNumFromA文字目の文字を復号化結果とする */
plain[i] = 'A' + shiftNumFromA;
} else if (cipher[i] >= 'a' && cipher[i] <= 'z') {
/* 小文字アルファベットの場合 */
/* 文字が'a'から何文字目の文字であるかを計算 */
numFromA = cipher[i] - 'a';
/* シフト */
shiftNumFromA = numFromA + shift;
/* shifNumFromAが負の値の場合は正の値に変換 */
while (shiftNumFromA < 0) {
/* 後で剰余算を行う数を足す */
shiftNumFromA += alphaLen;
}
/* アルファベット数以内に収める */
shiftNumFromA = shiftNumFromA % alphaLen;
/* 'a'からshiftNumFromA文字目の文字を復号化結果とする */
plain[i] = 'a' + shiftNumFromA;
}else {
/* アルファベット以外は変換しない */
plain[i] = cipher[i];
}
}
/* 最後の文字の後ろにヌル文字を付加する */
plain[strlen(cipher)] = '\0';
}
int main(int argc, char *argv[]) {
char plain[MAX_STR + 1];
char cipher[MAX_STR + 1];
if (argc != 3) {
printf("引数に暗号文とシフト量を指定してください\n");
return -1;
}
if (strlen(argv[1]) > MAX_STR) {
printf("平文が長すぎます\n");
return -1;
}
strcpy(cipher, argv[1]);
printf("暗号:%s\n", cipher);
decrypto(plain, cipher, atoi(argv[2]));
printf("平文:%s\n", plain);
return 0;
}
実行する際には、下記のように第1引数に暗号文、第2引数にシフト量を指定して実行します。実行すると暗号文をシーザー暗号で復号化した結果が表示されます。
$ ./decrypto.exe "Ht Ivhz Dn Tvhvyv Ovmj." 5 暗号:Ht Ivhz Dn Tvhvyv Ovmj. 平文:My Name Is Yamada Taro.
暗号化プログラムで暗号化した結果を第1引数に、暗号化プログラム実行時に指定したシフト量を第2引数に指定して実行すると、暗号化プログラム実行時に指定した第1引数の平文が表示されるはずです。
ただし、暗号化した時と異なるシフト量を指定すると実行して正しく複合できないので注意してください。
暗号化の時と同様に、暗号文は1つの引数に収まるように "
で括って実行してください。
復号化プログラム(総当たり)
ちなみに、先程紹介した復号化プログラムのソースコードにおける main
関数を下記のように変更すれば、シフト量を総当たり(アルファベットの文字数分)にして復号することができます。
int main(int argc, char *argv[]) {
char plain[MAX_STR + 1];
char cipher[MAX_STR + 1];
if (argc != 2) {
printf("引数に暗号文を指定してください\n");
return -1;
}
if (strlen(argv[1]) > MAX_STR) {
printf("平文が長すぎます\n");
return -1;
}
strcpy(cipher, argv[1]);
printf("暗号:%s\n", cipher);
/* 総当たりで復号してみる */
for (int i = 0; i < 26; i++) {
decrypto(plain, cipher, i);
printf("%d:%s\n", i, plain);
}
return 0;
}
この復号プログラムを実行すると、26
通りの復号結果が表示されます(シフト量はプログラム内のループで勝手に決まるのでプログラム実行時に指定する必要はありません)。
./all.exe "Ht Ivhz Dn Tvhvyv Ovmj." 暗号:Ht Ivhz Dn Tvhvyv Ovmj. 0:Ht Ivhz Dn Tvhvyv Ovmj. 1:Iu Jwia Eo Uwiwzw Pwnk. 2:Jv Kxjb Fp Vxjxax Qxol. 3:Kw Lykc Gq Wykyby Rypm. 4:Lx Mzld Hr Xzlzcz Szqn. 5:My Name Is Yamada Taro. 6:Nz Obnf Jt Zbnbeb Ubsp. 〜略〜
↑ 一部復号結果を略していますが、このように総当たりで復号すれば意味が分かる文章が1つ現れると思います。こんな感じで、シーザー暗号では、復号を行うためのシフト量が分からなくても総当たりで簡単に暗号が解かれてしまいます。なので、最初にも述べたように実用性は低いです。
まとめ
このページでは、まずシーザー暗号について解説し、続いてC言語でプログラミングした例を紹介しました。
シーザー暗号は平文の各文字をアルファベット順的にシフトすることで暗号化を行う暗号になります。また暗号化時と同じ分だけ逆方向にシフトすることで復号化を行うことができます。
シーザー暗号は総当たりで簡単に復号できてしまうので暗号としての実用性は低いですが、暗号がどのようなものかを実感する上では非常に良い題材だと思います。
また、文字のシフトや足し算や引き算で行うことで、C言語においては文字が数値として扱われている点も実感できると思います。
オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/