富山のホームページ製作会社・グリーク スタッフブログ - ブログ -
  1. グリークトップ
  2. PHP
  3. ブログ

写真の中に秘密のメッセージを含めてみる (ステガノグラフィー)

今の時代TwitterやInstagramなどで写真を見る機会が多くあると思います。
何気なく見ているその写真、本当にただの写真ですか?
スパイが写真に見せかけて暗号文をやりとりしてたら… などと考えるとわくわくしませんか?

ということでそんなことが可能かどうかやってみました。

結論:できる

今回の実験内容

このアルパカさんの写真にメッセージを埋め込んでみたいと思います。

alpaca

メッセージを加工する

まずはじめにメッセージをBASE64エンコードします。LinuxかMacの方はコマンドでやってください。

printf "アルパカはフェ〜と鳴くよ" | base64

するとこうなります。

44Ki44Or44OR44Kr44Gv44OV44Kn44Cc44Go6bO044GP44KI

これを1文字づつ次の手順で処理していきます。

  1. 各文字をそれぞれのアスキーコードに変換する。(たとえば aは97, 3は51)
  2. 変換したものを8桁の2進数に変換する。 (たとえば 97 は 01100001)
  3. メッセージの長さを保存する領域48ビット分を先頭に追加する。

するとこうなります。

0000000000000000000000000000000000000001100000000011010000110100010010110110
1001001101000011010001001111011100100011010000110100010011110101001000110100
0011010001001011011100100011010000110100010001110111011000110100001101000100
1111010101100011010000110100010010110110111000110100001101000100001101100011
0011010000110100010001110110111100110110011000100100111100110000001101000011
0100010001110101000000110100001101000100101101001001

赤字がメッセージの長さ(384ビット)、残りが加工されたメッセージです。

これをPHPで実装するサンプルです。

<?php

$message = 'アルパカはフェ〜と鳴くよ';
$encoded = base64_encode($message);
$binary  = '';

for ($i = 0; $i < strlen($encoded); $i++) {
    $binary .= sprintf('%08b', ord($encoded[$i]));
}

$length = sprintf('%032b', strlen($binary));
$binary = $length . $binary;

var_dump($binary);

これでメッセージの加工ができました。
あとはそれを画像に埋め込むだけです。

画像へ埋め込む仕組み

コンピューターで表現される写真は細かい点の集合でできています。
当然それらの点には色が含まれています。
各色はRGBカラーモデルで表現されていて、Webの世界では rgb(255,0,0)や#ff0000のように表記します。
ちなみにどちらも赤を表しています。

これは光の三原色 R(赤), G(緑), B(青) それぞれに8ビットを割り当てたものですので、#ff0000を2進数で表現するとこうなります。

11111111 00000000 00000000

この内の各色末尾1ビットをメッセージ保存領域として使ってしまおうという話です。
つまり画像の一つの点(1ピクセル)あたり3ビットの情報を保管するわけです。

11111111 00000000 00000000

これによって色が微妙に変化することになりますが、このレベルだと人間の目には全く識別できないです。

やってみる

ひきつづきPHPをつかってやってみます。
加工したメッセージを3ビットずつ取り出して、画像1ドットに保管していきます。

<?php

function generateBits()
{
    $message = 'アルパカはフェ〜と鳴くよ';
    $encoded = base64_encode($message);
    $binary  = '';

    for ($i = 0; $i < strlen($encoded); $i++) {
        $binary .= sprintf('%08b', ord($encoded[$i]));
    }

    $length = sprintf('%048b', strlen($binary));
    $binary = $length . $binary;

    foreach (str_split($binary, 3) as $part) {
        yield str_split($part);
    }
}

if (!$image = imagecreatefrompng('alpaca.png')) {
    throw new \RuntimeException('Unable to load png resource');
}

list ($width, $height) = getimagesize('alpaca.png');
$x = $y = 0;

foreach (generateBits() as $bits) {
    $rgb = imagecolorat($image, $x++, $y);
    $r = ($rgb >> 16) & 0xFF;
    $g = ($rgb >> 8) & 0xFF;
    $b = $rgb & 0xFF;

    $color = imagecolorallocate(
        $image,
        bindec(substr(decbin($r), 0, -1) . $bits[0]),
        isset($bits[1]) ? bindec(substr(decbin($g), 0, -1) . $bits[1]) : $g,
        isset($bits[2]) ? bindec(substr(decbin($b), 0, -1) . $bits[2]) : $b
    );

    imagesetpixel($image, $x, $y, $color);
    imagecolordeallocate($image, $color);

    if ($x > $width) {
        $x = 0;
        $y++;
    }
}

imagepng($image, 'alpaca_secret.png');
imagedestroy($image);

読み取る場合は

この仕様でメッセージを埋め込んだ場合の読み取り手順は

  1. 画像の左上から16ビクセル(48ビット分)取得し、メッセージの長さを取得
  2. メッセージの長さに必要な分残りのデータを取得
  3. 8ビットづつ10進数に戻して、ASCIIコード上の該当する文字に戻す
  4. すべてつなげたものをBASE64デコードする

完成品はこちら

Beforealpaca Afteralpaca_secret

実用化するにはいくつか足りない点がありますが、これが基本的な仕組みです。
必要なものはこのあたりでしょうか?

  • 画像に欠けがあっても復元できるように誤り訂正符号を含める
  • 画像のサイズとメッセージ量に応じて色の末尾何ビットを利用するか判断する

この技術を応用したものがいわゆる「電子透かし」ですね。

これをライブラリ化したものをGithubで公開しています。
https://github.com/kzykhys/Steganography

まとめ

情報を色として保存することで、秘密のメッセージを画像に含めることができました。
今回の実験はこれで終了です。

ね、簡単でしょう?

この記事を書いたスタッフ
Kazuyuki Hayashi
Symfony / Node.js / React.js / Swift を中心にテクノロジー系の記事を書きます。趣味はコーヒーです。