Goでステガノグラフィやってみる

こんにちは、痩せるが口癖の3年目の終わりが近いエンジニアの井村です。
ゲーム事業グループ内でGo勉強会を隔週でやっていまして第4回目の時に担当になったのでその内容を書こうと思います。

勉強会

まず、前提として私のGo言語の経験は勉強会時点で「スターティングGo言語」のChapter5までの基本構文を読んだ程度です。

今回の勉強会ではステガノグラフィをテーマとしました。
画像データに別のデータ(画像・文字列)を埋め込み、その方法を参加者に教えてそれを復元してもらいました。

ステガノグラフィとは

ステガノグラフィはデータ隠蔽の一種で画像、音声、テキストなどのデータを別のデータに隠す技術のことです。
今回の勉強会ではLSBステガノグラフィと呼ばれる方法を使いました。

LSB法

各RGB値の最下位ビット(LSB)にデータを埋め込む方法。
RGB値の最下位1bitを使う場合、1ピクセルに3bit使うことができます。

以下の画像は中央の画像を基準に左が最上位bitを変化させたもの、右が最下位bitを変化させたものです。
左の画像は肉眼でも色が変化していることがわかりますが、右の画像は肉眼では変化がわかりません。
このように下位bitを使って、人の眼では認識できないようにしてデータを隠します。

RGB(127,0,0)RGB(255,0,0)RGB(254,0,0)
R(01111111)R(11111111)R(11111110)

今回、下位何bit使うかは実際に画像を作ってみて判断しました。
以下の画像はそれぞれ下位6bit、4bit、2bit使ってデータを埋め込んだ画像です。
6bitと4bitは肉眼でも何かが見えてしまっています、2bitはギリギリ見えないだろうと判断して2bitに決めました。

6bit4bit2bit

imageパッケージ

Image interfaceは画像データを抽象化してくれるものでパッケージにはgif、jpeg、png形式のデコーダ・エンコーダが用意されています。
自作のデコーダを作成してimage.RegisterFormatに渡すことで独自の画像形式に対応することも可能です。

PNG読み込み

package main

import (
    "image"
    "image/png"
    "os"
)

func loadPNG(path string) (image.Image, error) {
    // ファイルを開く
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    // ファイルをデコードする
    img, err := png.Decode(file)
    if err != nil {
        return nil, err
    }
    return img, nil
}

PNG保存

package main

import (
    "image"
    "image/png"
    "os"
)

func savePNG(path string, img image.Image) error {
    // ファイルを作成
    file, err := os.Create(path)
    if err != nil {
        return err
    }
    defer file.Close()

    // 画像データをエンコードする
    png.Encode(file, img)
    return nil
}

画像作成

image.NewNRGBAでNRGBA構造体のポインタが返ってきます。
またimage.NewRGBAでRGBA構造体のポインタが返ってきます。
2つの違いはアルファ乗算済みかどうかです。

package main

import (
    "image"
    "image/color"
)

fun main() {
    // widthが256、heightが256のNRGBAを作成
    img := image.NewNRGBA(image.Rect(0, 0, 256, 256))

    rect := img.Bounds()
    // すべてのピクセルを赤色にする
    for y := rect.Min.Y; y < rect.Max.Y; y++ {
        for x := rect.Min.X; x < rect.Max.X; x++ {
            img.SetNRGBA(x, y, color.NRGBA{255, 0, 0, 255})
        }
    }

    // 画像書き出し
    savePNG("new.png", img)
}

データ埋め込み

実装を簡単にするためにカバー画像と隠蔽画像は同じサイズ、アルファ値は255に固定して書き出しています。

全ピクセルに対して以下の処理をします。

r := coverColor.R&0xfc + sourceColor.R>>6
g := coverColor.G&0xfc + sourceColor.G>>6
b := coverColor.B&0xfc + sourceColor.B>>6

ここではカバー画像をマスクして上位6bitを取り出し、隠蔽画像を右シフトして下位2bitにズラしたものを加算しています。

カバー画像隠蔽画像出力画像
R
G
B
func lsbEncode() {
    // カバー画像の読み込み
    coverImg, err := loadPNG("cover.png")
    if err != nil {
        log.Fatal(err)
    }
    // 隠蔽画像の読み込み
    sourceImg, err := loadPNG("source.png")
    if err != nil {
        log.Fatal(err)
    }

    rect := coverImg.Bounds()
    if rect != sourceImg.Bounds() {
        log.Fatal("画像サイズが一致しません")
    }
    newImg := image.NewNRGBA(image.Rectangle{rect.Min, rect.Max})

    for y := rect.Min.Y; y < rect.Max.Y; y++ {
        for x := rect.Min.X; x < rect.Max.X; x++ {
            c := color.NRGBAModel.Convert(coverImg.At(x,y))
            s := color.NRGBAModel.Convert(sourceImg.At(x,y))
            coverColor, ok := c.(color.NRGBA)
            if !ok {
                continue;
            }
            sourceColor, ok := s.(color.NRGBA)
            if !ok {
                continue;
            }
            r := coverColor.R&0xfc + sourceColor.R>>6
            g := coverColor.G&0xfc + sourceColor.G>>6
            b := coverColor.B&0xfc + sourceColor.B>>6

            newImg.SetNRGBA(x, y, color.NRGBA{r, g, b, 255})
        }
    }
    savePNG("lsb.png", newImg)
}
カバー画像隠蔽画像1出力画像

データ抽出

抽出は埋め込みの逆の処理で、各ピクセルのRGB値を左に6シフトして下位2bitを上位2bitにするだけです。

r := c.R<<6
g := c.G<<6
b := c.B<<6
入力画像出力画像
R
G
B
func lsbDecode() {
    img, err := loadPNG("lsb.png")
    if err != nil {
        log.Fatal(err)
    }

    rect := img.Bounds()
    newImg := image.NewNRGBA(image.Rectangle{rect.Min, rect.Max})

    for y := rect.Min.Y; y < rect.Max.Y; y++ {
        for x := rect.Min.X; x < rect.Max.X; x++ {
            i := color.NRGBAModel.Convert(img.At(x, y))
            c, ok := i.(color.NRGBA)
            if !ok {
                continue;
            }
            r := c.R<<6
            g := c.G<<6
            b := c.B<<6
            newImg.SetNRGBA(x, y, color.NRGBA{r, g, b, 255})
        }
    }
    savePNG("decode.png", newImg)
}
入力画像出力画像1

最後に

今回の実装では簡単にするために画像サイズが同じで各ピクセルが対応していました。さらに、上位2bitだけを使うので元データの6bit分のデータを破棄しています。
興味のある人はコードを修正して並列化したり、元データを完全に埋め込む方法を考えたりしてみてください。

今回の勉強会の担当になったことでGo言語とステガノグラフィという2つの新しいことに触れられたので楽しかったです。今後、ステガノグラフィについては個人的な趣味として勉強していきたいと思います。

以下にGo Playgroundで動くようにしたものを貼っておきます。
The Go Playground

  1. オリジナルのThe Go gopher(Gopherくん)は、Renée Frenchによってデザインされました。


関連記事一覧

  1. この記事へのコメントはありません。