シーザー暗号で画像を暗号化したかった


まず本題に入る前にシーザー暗号について分からない方に向けて簡単に説明します。分かる方は次の 「どのように画像を暗号化するのか」 まで飛ばしてください。

シーザー暗号とは

シーザー暗号は暗号の中では、かなりシンプルな仕組みで暗号化したい平文を特定の文字ずつずらしていくものです。例を挙げると「こんにちは」という平文を3文字ずつずらすと「すうのとふ」という風になります。かなり元の文と変わりましたね。また、全部の文字を同じ数だけずらすだけでなく、ある鍵となる1つの文章を決めて平文の1つ目の文字には鍵の1つ目の文字、平文の2つ目の文字には鍵の2つ目の文字をずらすことで、全部を同じ数だけずらすやり方では、ひらがな50音の50回繰り返せば復元が出来てしまっていたのを、鍵となる文章を使い1つずつ対応させ暗号化することで復元を難しくすることが出来ます。上記がシーザー暗号の大まかな仕組みです。

シーザー暗号 Wikipedia

シーザー暗号は単一換字式暗号の一種であり、平文の各文字を辞書順で3文字分シフトして(ずらして)暗号文とする暗号である。古代ローマの軍事的指導者ガイウス・ユリウス・カエサル(英語読みでシーザー)が使用したことから、この名称がついた。
文字のシフト数は固定であるが、3に限る必要はない。たとえば左に3文字分シフトさせる場合、「D」は「A」に置き換わり、同様に「E」は「B」に置換される。
引用元:シーザー暗号 Wikipedia

どのように画像を暗号化させるのか

まず画像には解像度という縦何ピクセル、横何ピクセルのといった画像に含まれる色の点の数を表す物があります。その1つ1つのピクセルには色が決められていて画像ファイルには1ピクセルずつの色の情報が含まれています。(画像形式によっては含まれていないものもある)

そこで私は1つ1つの色のピクセル情報が書いてあるRGBA情報をシーザー暗号で暗号化すれば、パスワードを知らない限り中身が分からない画像を作れるのではないかと思いました。

RGBAとは

色を表すための手法の1つ。3原色のRed(赤),Green(緑),Blue(青)の組み合わせの強度とAlpha(透明度)をそれぞれ256段階で表している。

似たサービスについて

またこのようなサービスですぐに思いつくものとしてGoogleDrive等に保存してパスワードを掛ければパスワードを知っている人しか見れないしそれでいいじゃんと思う人もいるかも知れませんが、その方法では画像はGoogleDriveに保存されているので多くの人からは見れないようになっていますがGoogleさんからは見られてしまいます。しかし、今回考えたものではどこにも元画像は保存されません。なので確実にプライベードを保ったまま暗号化できます。さらに、皆が暗号化された画像を見ることが出来ているのにパスワードを知らないから中身が分からない。それがなんかカッコいいと思っています。

使用言語

誰でもすぐに使えるようにwebで動くページを作ろうと思いました。
その為、HTML, CSS, JSを使い実装しました。

実装

完成後の見た目

まず今後のイメージがしやすいようにページの全体的な見た目を貼っておきます。CSS頑張ってないです...


まず暗号化する画像を選択するプログラムを書きました。
そこであることを思い出し、画像形式に対する制限を付けました。

使用する画像形式

良くTwitterとかでカピカピの画像を見ませんか?
あれの原因は何回も保存を繰り返すことで画質が劣化していっているためです。この画像の仕組みを非可逆圧縮と言います。また保存を繰り返しても画質が劣化しない画像の仕組みを可逆圧縮と言います。
画像形式によって、仕組みが違うのでいくつかあげます。

GIF PNG PDF JPG WebP
可逆圧縮
非可逆圧縮

画質が劣化したら暗号化したピクセルがずれてしまい、復元化出来なくなってしまうのではないかと考え、画像形式をPNGに限定することにしました。

選択した画像をCanvasで表示する

html
<input type="file" id="selectFile" accept=".png">

accept属性でpngに限定して選択が出来るようにしています。

js
// 要素を指定
var selFile = document.getElementById('selectFile');

selFile.addEventListener("change", function (evt) {
    var file = evt.target.files;
    var reader = new FileReader();
    //dataURL形式でファイルを読み込む
    reader.readAsDataURL(file[0]);
    reader.onload = function () {
	var dataUrl = reader.result;
	var img = new Image();
	img.src = dataUrl;
	img.onload = function () {
	    ...
	}
    }
}

まず要素を指定して、それに変化が起きた場合に動く関数を作ります。その中でdataURL形式でファイルを読み込みます。そして読み込まれた時にそのdataURLをimg.srcに入れます。
するとdrawImageで指定できるイメージ(img要素, canvas要素, video要素)のimg要素になったので、後は煮るなり焼くなり好きにして下さい。

私の場合は、画像を選択してる上でもう一度選択した際に画像の解像度が違うと元の画像がはみ出て見える場合があるので、clearRectで一度綺麗にしてからdrawImageで貼り付けてます。

参考URL

画像のピクセル情報を獲得する

js
var imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;

Canvasに描画しているものをgetImageDataでImageDataオブジェクトをゲットします。そしてImageData.dataで全てのピクセルのRGBA情報をゲットできます!凄い!
試しにコンソールで見てみます。上から1ピクセルずつの符号なしの0~255で表されるRGBAの情報が返ってきました!(1つ目の値がR、2つ目がG、3つ目がB、4つ目がA、5つ目は2ピクセル目のR...)

Uint8ClampedArray(1088000) [111, 50, 49, 255, 97, 37, 31, 255, 130, 72, 58, 255, 81, 25, 9, 255, 76, 20, 12, 255, 70, 15, 11, 255, 120, 67, 66, 255, 59, 7, 1, 255, 54, 3, 0, 255, 114, 62, 39, 255, 148, 93, 70, 255, 74, 16, 0, 255, 86, 25, 6, 255, 84, 20, 0, 255, 122, 58, 27, 255, 176, 115, 75, 255, 112, 57, 9, 255, 158, 102, 51, 255, 152, 90, 41, 255, 130, 60, 15, 255, 135, 57, 18, 255, 153, 69, 36, 255, 112, 24, 0, 255, 155, 65, 42, 255, 128, 38, 19, 255, …]

参考URL

シーザー暗号を使う前にテスト(悲劇)

いきなりシーザー暗号を使う前に、全ての値を同じ数だけずらしてみようと思いました。そしてやってみたら驚きのことが発覚しました。
今考えると当たり前なのですが、作業ハイになっていたのでしょう。RGBAは強度を表しているので、値を5変えたところで色合いが少ーーーーし変わるくらいにしか変化しなかったのです。

悲劇からの脱却

そこで思いついたのが、0~255がランダムな順番になっている配列を使うことで値は大きく変わるのではないかと思いました。

var r = [68, 153, 31, 200, 188, 207, 105, 216, 22, 159, 55, 210, 245, 205, 219, 241, 70, 204, 214, 167, 249, 67, 238, 91, 46, 253, 147, 218, 144, 150, 108, 211, 75, 132, 120, 93, 101, 251, 252, 189, 63, 237, 124, 117, 40, 138, 187, 52, 148, 146, 71, 161, 6, 26, 192, 87, 41, 129, 246, 248, 232, 80, 174, 208, 170, 227, 212, 176, 51, 183, 175, 172, 1, 229, 10, 121, 171, 145, 77, 195, 230, 54, 76, 221, 151, 202, 228, 109, 247, 165, 164, 126, 149, 103, 11, 193, 53, 234, 107, 62, 222, 255, 100, 27, 203, 35, 83, 239, 143, 33, 156, 224, 157, 122, 254, 45, 116, 194, 72, 130, 94, 231, 134, 44, 92, 209, 59, 118, 36, 106, 223, 119, 39, 184, 64, 125, 89, 142, 112, 15, 56, 140, 66, 74, 81, 182, 177, 158, 96, 20, 191, 213, 111, 131, 137, 127, 163, 186, 42, 190, 84, 3, 69, 61, 162, 0, 19, 90, 13, 139, 110, 152, 180, 136, 128, 244, 5, 114, 17, 199, 73, 60, 99, 168, 85, 43, 243, 2, 24, 235, 196, 28, 7, 82, 50, 86, 181, 58, 178, 38, 79, 173, 242, 29, 154, 18, 37, 169, 49, 160, 141, 104, 185, 225, 78, 97, 23, 65, 233, 240, 47, 115, 198, 133, 113, 217, 123, 166, 206, 8, 9, 57, 25, 14, 226, 220, 201, 98, 34, 179, 215, 88, 30, 102, 48, 16, 95, 32, 250, 135, 21, 155, 4, 236, 12, 197];
var g = [195, 242, 137, 221, 250, 240, 12, 248, 27, 25, 32, 203, 243, 10, 119, 62, 200, 231, 179, 80, 175, 219, 29, 189, 117, 88, 9, 1, 73, 212, 174, 207, 255, 63, 72, 129, 13, 142, 148, 216, 160, 169, 79, 2, 146, 47, 30, 108, 158, 54, 103, 199, 122, 131, 45, 155, 202, 76, 217, 20, 60, 43, 194, 170, 111, 40, 61, 224, 90, 124, 100, 214, 244, 114, 238, 50, 229, 191, 3, 83, 96, 177, 165, 128, 107, 14, 211, 220, 159, 15, 48, 247, 183, 18, 180, 16, 87, 140, 121, 145, 154, 7, 172, 232, 153, 226, 66, 241, 19, 91, 254, 178, 106, 182, 208, 201, 58, 74, 171, 239, 127, 41, 82, 51, 173, 134, 95, 138, 213, 135, 52, 8, 235, 35, 167, 225, 68, 186, 192, 37, 125, 162, 168, 147, 181, 245, 110, 28, 253, 98, 94, 26, 5, 57, 102, 196, 166, 64, 24, 237, 130, 21, 164, 176, 204, 81, 136, 197, 252, 105, 39, 118, 97, 139, 77, 22, 33, 17, 101, 156, 112, 185, 230, 53, 92, 46, 109, 93, 190, 4, 113, 236, 234, 56, 210, 104, 115, 38, 215, 42, 11, 86, 222, 89, 126, 71, 116, 233, 99, 65, 75, 69, 161, 55, 151, 0, 141, 123, 198, 218, 144, 67, 49, 187, 157, 44, 120, 85, 31, 132, 223, 143, 188, 209, 251, 70, 206, 59, 228, 36, 133, 246, 249, 184, 149, 34, 23, 163, 227, 150, 193, 205, 152, 6, 78, 84];
var b = [87, 187, 101, 68, 79, 125, 23, 54, 83, 129, 198, 185, 182, 6, 146, 94, 197, 40, 28, 69, 145, 194, 240, 192, 140, 143, 179, 22, 235, 150, 96, 175, 248, 251, 213, 205, 139, 241, 236, 167, 242, 134, 1, 245, 4, 116, 74, 151, 195, 111, 166, 222, 122, 144, 73, 157, 103, 108, 115, 162, 86, 57, 234, 226, 121, 80, 160, 164, 9, 120, 85, 174, 216, 178, 118, 250, 193, 244, 11, 44, 3, 36, 91, 106, 155, 208, 170, 109, 15, 119, 61, 128, 142, 147, 239, 81, 89, 181, 0, 64, 249, 238, 16, 152, 88, 180, 42, 48, 212, 184, 156, 183, 210, 32, 201, 138, 158, 114, 24, 99, 171, 161, 132, 221, 29, 47, 190, 214, 31, 199, 254, 112, 56, 66, 2, 165, 7, 228, 229, 10, 65, 127, 104, 133, 191, 123, 246, 211, 186, 102, 203, 100, 43, 13, 39, 149, 247, 223, 126, 169, 49, 159, 135, 5, 72, 253, 207, 252, 38, 63, 59, 172, 224, 188, 141, 46, 113, 77, 130, 97, 71, 204, 75, 60, 177, 25, 30, 136, 153, 154, 107, 52, 105, 98, 176, 90, 19, 202, 124, 137, 168, 227, 148, 55, 215, 217, 110, 243, 8, 95, 27, 14, 67, 230, 218, 50, 70, 76, 225, 117, 58, 12, 82, 237, 92, 35, 189, 93, 209, 37, 220, 131, 51, 84, 173, 34, 78, 18, 231, 233, 33, 206, 219, 20, 232, 200, 17, 196, 21, 26, 41, 45, 163, 53, 62, 255];
for (var i = 0, len = data.length; i < len; i += 4) {
    data[i] = r[data[i]];
    data[i + 1] = g[data[i]];;
    data[i + 2] = b[data[i]];;
    data[i + 3] = 255;
}

結果

おー!予想以上に上手く動作しました。嬉しかったです。

しかし良く見てみると猫の面影が見えてきました。段々と目が見えてきて鼻が見えて輪郭が見えてきました...

ですが一旦そのことは忘れて保存と復元化システムを作りました。

保存と復元化

保存

html
<a id="download" href="#" onclick="saveCanvas();" download="canvas.png">ダウンロード</a>
js
function saveCanvas() {
    document.getElementById("download").href = hideCanvas.toDataURL("image/png", 1.0);
}

html要素がクリックされて時にCanvasをtoDataURLで指定した画像形式のdata URIを返すメソッドを使い、ダウンロードするようにしました。

復元化

for (var i = 0, len = data.length; i < len; i += 4) {
    data[i] = r.indexOf(data[i]);
    data[i + 1] = g.indexOf(data[i + 1]);
    data[i + 2] = b.indexOf(data[i + 2]);
    data[i + 3] = 255;
}

このやり方は暗号化でした逆のことをすれば復元化出来るので、冷静に逆のことをするプログラムを書いて実行すると...

上手く復元化することが出来ました!

シーザー暗号の鍵を入力する

html
<input type="text" id="inputText" onInput="textCheck(this)">
js
function textCheck($this) {
    str = $this.value;
    // 入力した文字が0-9a-zA-zになっていない場合
    if (!(str.match(/^[0-9a-zA-Z]+$/))) {
        str = "";
    }
    $this.value = str;
}

これで大文字小文字、数字を入力以外を入力すると入力が自動的に消されるようになりました。
ちなみに大文字小文字、数字は62種類の文字数になります。これらのパターンは10文字入力するだけ8.3929937e+17個という結果が出てきます。e+17は0が17個付いてるよということなので800000000000000000個のパターンということになります。凄い!

あれこれもしかして...

作っているうちに先ほどの暗号化した画像のなんとなく形が分かってしまう問題について考えていました。
なぜあの問題が起きてしまうかというと、同じ色の場合、同じ配列の場所を取ってくるので同じ色になってしまいます。同じ色が固まっている場所では目立ってしまい目や鼻、輪郭が分かってしまう現象が起きていました。
分かりやすく色が少ない画像で暗号化してみます。

あー、やはり。私の画力はさておき、色が同じだと暗号化してもホラーバージョンになるだけで、暗号化出来ていない...と分かりました。

この問題から脱却する方法とは

この問題から脱却するには暗号化の鍵をとても長くして同じ場所が来ても、そのパスワードの文字数文/3(1ピクセルRGBで3つの情報を使う Aは255で固定なので)の長さのピクセルなら対応できるようにする、という方法をまず思いつきました。
しかしこれは個人的に微妙かなと思っています。理由は、仮に上の画像のように色が少ない場合は本当に本当に長い鍵を作らないと行けないからです。

別の解決法を考えていたところ、画像の圧縮のやり方を思い出しました。これは同じピクセルが連続するときには先に同じピクセルの数を書き次に共通するrgba情報を書くというやり方です。例えば8つ同じピクセルが続く場合は 8 r g b a のように書くことで同じピクセル情報が連続して書かれないようにする。というやり方です。

このやり方ではrgbaのa(透明度)を255にしたかったので、毎回1つずつずれるようになってしまいます。これだとrgbaの固まり4つで被らない3つのピクセルを表すことが出来ます。

更に色々画像を暗号化して分かったのですが、大体のピクセルは被っていないことが多いのでこの方法だとピクセル数を表す0が多くなってしまいます。それで鍵を使い暗号化するとピクセルの数を表すところで同じ値になる場所が多くなってしまいます。そのことから文字数が把握出来ます。更に、暗号に強い人が見たらもっと色々な情報が分かりそうで、このやり方だと弱いかなと思いました。

他にも色々考えてみたのですが、私には上手い解決法を見つけることが出来ていません。
誰か良い方法を思いつくことが出来たら、TwitterのDMかコメントして教えて頂けると嬉しいです。

反省点

曖昧なイメージのまま見切り発車でコードを書き始めてしまった点。しっかりと仕組みを考えてから作り始めれば良かったです。
しかし初めて知ることもあったのでやってみて損ではなく得したかなと思っています。
更に、このやり方を使えば動画を画像化も出来るかなと思いつくことも出来ました。動画を特定の時間ごとに画像を取ってきてRGBA情報をくっつけたら1枚の画像で動画を表せるかなと思いました。そしてその画像を選択したら動画が再生出来るようになるかな、と思いました。けど使う用途が思い浮かばないのですぐには作らないで良いかなと思っています。

まとめ

結果、最終的に「この問題から脱却する方法とは」に書いたようにやり方を思いつくことが出来ず8割くらいの完成度となりました。
色の塊が少ない複雑な画像だと暗号化が出来るプログラムの完成になりました。

GitHub

GitHubで全体のコードを公開しています。興味のある人は是非。

https://github.com/fukurotani/ceaserPics