Webでよく使われるPNG画像形式というのは、比較的単純で拡張性の高いファイル形式となっています。今回は拡張性の高さを活かして、PNG画像の中に暗号文を書き込むというプログラムを作ってみましょう。

  • PNG画像に暗号文を埋め込むプログラムを実行したところ

    PNG画像に暗号文を埋め込むプログラムを実行したところ

PNG画像の暗号文を埋め込むプログラムを作ろう

前回、PNG画像を例にGo言語でバイナリ操作を行う方法を紹介しました。しかし、前回作ったプログラムは、ただファイルを読み込み、画像ファイルのサイズを表示するだけでした。そこで、今回はPNG画像の拡張性の高さを利用して画像に暗号文を書き込むというプログラムを作ってみましょう。

PNG画像にテキストを埋め込むことが出来れば、画像に見せかけた秘密のメッセージを送受信したり、メッセージ付きのPNG画像を作るのに利用できます。

PNG画像について復習

既に前回紹介していますが、PNG画像の仕様を改めて確認してみましょう。PNGファイルというのは、以下のように、PNGシグネチャに続いて複数のチャンクと呼ばれるデータブロックが連続するという仕組みになっていました。

  • PNGファイルの構造 - 前回の復習

    PNGファイルの構造 - 前回の復習

それで、今回、暗号化したテキストをPNG画像に埋め込むのですが、適当な「xANG」というチャンクを独自定義して、PNGファイルにこのチャンクを埋め込むようにします。そして、このチャンクには、XORで簡易暗号化したテキストをデータ部分に設定するようにします。

  • 独自定義のチャンクをPNGに埋め込む

    独自定義のチャンクをPNGに埋め込む

しかし、適当な独自チャンクを埋め込んでしまっても問題ないのでしょうか。多くのPNG画像を表示する画像ビューワーでは、PNGファイルを読んでいる時、不明なチャンクを見つけても、そのチャンクを読み飛ばすことになっています。そのため、適当に独自チャンクをでっち上げても大丈夫という訳なのです。

今回作ったプログラムで作成したPNGファイルを、WebブラウザやOS標準の画像ビューワーなどで表示してみましょう。全く問題なく表示できます。

PNG画像を読み込むために構造体を準備

それでは、プログラムの作成に取りかかりましょう。前回、PNGファイルを読み込みましたが、PNGファイルを読むだけで書き込むことを考えていませんでした。そこで、PNGのチャンクを覚えておくために、Chunk構造体を定義して、読み込んだデータをこの構造体に保存していくようにしましょう。ここで定義するChunk構造体は、以下のようなものにします。

type Chunk struct {
    Size uint32 // チャンクサイズ(4byte)
    Type []byte // チャンクタイプ(4byte)
    Data []byte // データ本体(可変長)
    Crc  uint32 // CRC32の値(4byte)
}

構造体の定義を見て分かるようにPNGの各チャンクは固定長ではありません。ただし、本来Typeの型は、スライスではなく、[4]byteのように固定配列にするのが正しいのですが、プログラムの簡易化のためにbyte型のスライスとして定義しています。

PNGファイルを読み込もう

それでは、PNGファイルを読み込み、構造体Chunkのスライスとして返すプログラムは、以下のようになります。

// PNGのシグネチャ
const pngSignature = "\x89PNG\r\n\x1a\n"

func ReadPNGFile(fname string) ([]Chunk, error) {
  // ファイルを全部メモリに読み込む --- (*1)
  buf, err := ioutil.ReadFile(fname)
  if err != nil {
    return nil, fmt.Errorf("ファイル読み込みエラー")
  }
  // 先頭のPNGシグネチャを取り込む --- (*2)
  r := bytes.NewReader(buf)
  if string(readN(r, 8)) != pngSignature {
    return nil, fmt.Errorf("PNGファイルではありません")
  }
  // 複数のチャンクを繰り返し読む --- (*3)
  res := []Chunk{}
  for {
    // サイズ、タイプ、データ、CRCを順に読む --- (*4)
    chunk := Chunk{}
    chunk.Size = readInt32(r)
    chunk.Type = readN(r, 4)
    chunk.Data = readN(r, int(chunk.Size))
    chunk.Crc = readInt32(r) // CRC32
    if len(chunk.Type) == 0 {
      break
    }
    // 暗号文が埋め込まれていれば画面に表示 --- (*5)
    if string(chunk.Type) == "xANG" {
      text := xorText(string(chunk.Data), password)
      println("[TEXT]", text)
    }
    res = append(res, chunk) # --- (*6)
  }
  return res, nil
}

プログラムの(*1)の部分では、PNGファイルの内容をメモリに読み込みます。そして、(*2)ではPNGシグネチャが正しいかどうかを確認します。そして、(*3)の部分では繰り返しチャンクを読み込みます。(*4)の部分で実際にメモリから、チャンクサイズ、タイプ、データ、CRCの各値を読み込みます。そして、読み込んだチャンクを(*6)でスライスに追加します。

このとき、(*5)の部分にあるように、チャンクタイプが「xANG」という独自チャンクであれば、暗号を解除して画面に表示するようにしています。

PNGファイルを作成しよう

次に、ファイルから読み込んだデータ(Chunk構造体のスライス)を元にして、新たにPNGファイルに保存するプログラムを見てみましょう。読み込みよりも書き込みの方が簡単です。

func WritePNGFile(fname string, chunks []Chunk) error {
  // 書き込み用のバッファを作る --- (*1)
  buf := bytes.NewBuffer([]byte{})
  // シグネチャを書き込む --- (*2)
  buf.Write([]byte(pngSignature))
  // 繰り返しチャンクを書き込む --- (*3)
  for _, chunk := range chunks {
    if string(chunk.Type) == "IEND" {
      continue
    }
    writeUInt32(buf, chunk.Size) // サイズを書き込む
    buf.Write(chunk.Type) // タイプを書き込む
    buf.Write(chunk.Data) // データを書き込む
    writeUInt32(buf, chunk.Crc) // CRCを書き込む
    println("write:", string(chunk.Type))
  }
  // IENDを最後に書き込む --- (*4)
  writeUInt32(buf, 0)
  buf.Write([]byte("IEND"))
  buf.Write([]byte{0xAE, 0x42, 0x60, 0x82})
  return ioutil.WriteFile(fname, buf.Bytes(), 0644)
}

プログラムの(*1)の部分では、書き込み用のバッファを用意します。そして、(*2)ではPNGシグネチャを書き込みます。

そして、(*3)の部分で繰り返しチャンクを書き込んで行きます。チャンクのサイズ、タイプ、データ、CRC値と読み込んだ時と同じように書き込むだけです。そして、最後(*4)にPNGファイルの終端を示すIENDチャンクを追加してからファイルに書き込みます。ただし、PNGファイルの一番最後にIENDチャンクが配置されることになっています。そのため、(*3)のfor文の中では、IENDチャンクを書き込むことをせず、for文の後ろ(*4)でIENDチャンクを書き込むようにしています。

独自チャンクを追記しよう

続いて、独自チャンクを追記するプログラムを見てみましょう。PNGファイルからChunk構造体のスライスを読み込ん後で、以下のappendAngouChunk関数を用いて、テキストを暗号化して追記します。

// 独自チャンクを追加
func appendAngouChunk(chunks []Chunk, text string) []Chunk {
  chunk := Chunk{ // 構造体に初期値を設定
    Size: uint32(len(text)),
    Type: []byte("xANG"),
    Data: []byte(xorText(text, password)),
  }
  checkData := append(chunk.Type, chunk.Data...)
  chunk.Crc = crc32.ChecksumIEEE(checkData)
  return append(chunks, chunk)
}

ここでは、Chunk構造体を作成し、サイズ、タイプ、データ、CRC値を設定したら、ファイルから読み出したチャンクのスライスにそれを追加するというものになっています。ここでは、独自の"xANG"を追記するものですが、この部分を自分で書き換えるなら、独自のチャンクタイプを利用して、PNGに独自データを埋め込むことができます。

プログラムを実行してみよう

以上、ここまで作ったプログラムを合わせたものを、こちらにアップしています。ZIPファイルをダウンロードして解凍すると、Goのソースファイル「pngangou.go」とテスト用のPNG画像「tea.png」が生成されます。

以下、コマンドライン上で操作を行います。WindowsではPowerShellで、macOSではターミナル.appを利用して操作してください。

コマンドラインに以下のコマンドを実行すると、Windowsでは「pngangou.exe」(macOSでは「pngangou」)という実行ファイルが生成されます。

go build pngangou.go

それで、生成された実行ファイルに対して、「(入力ファイル) (出力ファイル) (テキスト)」のような引数を与えると、テキストを暗号化してPNGファイルに埋め込みます。例えば、「tea.png」に「捜すのに時あり、 諦めるのに時がある」というテキストを暗号化して埋め込んでみましょう。

# Windowsの場合
.\pngangou.exe tea.png out.png "捜すのに時あり、 諦めるのに時がある"




# macOSの場合
./pngangou tea.png out.png "捜すのに時あり、 諦めるのに時がある"

そして、埋め込んだテキストを復号化して読むには、以下のようなコマンドを実行します。つまり、実行時引数が3つある場合はファイルを暗号化し、1つの場合は復号化して表示します。

# Windowsの場合
.\pngangou.exe out.png



# macOSの場合
./pngangou out.png

プログラムを実行すると、PNGファイルに埋め込んだテキストを取り出して復号化して以下のように画面に表示します。

  • プログラムを実行したところ

    プログラムを実行したところ

まとめ

以上、今回はPNG画像にテキストを暗号化して埋め込むプログラムを作ってみました。PNGの拡張性の高さを利用したものなので、画像ファイルの中に何かしらの設定データを埋め込むプログラムの参考になるでしょう。また、ここでは、コマンドラインからテキストを埋め込むというプログラムを作りましたが、改良して任意のファイルなどを埋め込めるようにすると、より実用度が高くなると思います。参考にしてください。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ、2010年 OSS貢献者章受賞。技術書も多く執筆している。