🔑

Go言語で画像のsteganography (電子透かし)を実装しよう!(標準ライブラリのみで)

2022/07/19に公開

Golangの標準ライブラリで画像のRGB値の操作が柔軟にできるので、今回はRGB値の代わりに文字データを埋め込むステガノグラフィー(以下、電子透かしで呼称)をGo言語で実装したいと思います。

基本設計

  • 対象ファイルは可逆ファイルのPNGファイルのみ
  • JPEGファイルは基本は不可逆なので別のアプローチが必要になる(今回は対象としない)
  • PNGの拡張データや、JPEGのEXIFなどでも電子透かしは埋め込めるが、除去も簡単なので今回は画像そのものを改変するアプローチをとった
  • 同様にロゴ(ウォーターマーク)も除去するソフトも出回っているため今回は別アプローチとした。
  • 画像改変をするアプローチは画像データを一部損なうのでデメリットであるが、電子透かしの除去が難しいというメリットがある

詳細設計

指定されたPNGファイルのRGB値を変更し、指定された文字列のバイトデータを埋め込みます。 デフォルトでは x,y座標の先頭からy=0を固定で1行目を書き換えます。 たとえば ABCであれば 1,0の座標データを書き換えます (1ピクセルはRGBAの4バイトのためx=1以内)。 また、RGBAのAはデータ埋め込みに使用しないため1座標に埋め込めるのは3バイトになります。 このためx軸(横の長さ[width])*3バイトまでの文字列を埋め込むことが可能です。

  • ABC -> x,y=[1,0]を書き換え (3byte) (※加えて終端として[2,0]を[RGBA]=[0,0,0,255]で変更、以下も同じ)
  • ABCD -> x,y=[1,0][2,0]を書き換え (4byte)
  • ABCDE -> x,y=[1,0][2,0]を書き換え (5byte)
  • ABCDEFG -> x,y=[1,0][2,0][3,0]を書き換え (7byte)

プログラム例

package main

import (
	"flag"
	"fmt"
	"image"
	"image/color"
	_ "image/jpeg"
	"image/png"
	"log"
	"os"
	"strconv"
)

func main() {

	var filePath string
	var embeddedText string
	var detailView bool
	var decode bool
	var copy bool
	flag.BoolVar(&copy, "c", false, "プログラム検証用 rgb copy")
	flag.BoolVar(&decode, "d", false, "decode")
	flag.StringVar(&filePath, "f", "", "filePath")
	flag.StringVar(&embeddedText, "t", "", "embedded Text [max = image width * 3byte]")
	flag.BoolVar(&detailView, "v", false, "detailView")

	flag.Parse()

	file, err := os.Open(filePath)
	if err != nil {
		log.Fatalf("failed to open file: %s", err.Error())
	}
	defer file.Close()
	img, format, err := image.Decode(file)
	if err != nil {
		log.Fatalf("failed to decode image: %s", err.Error())
	}
	//フォーマット名表示
	fmt.Println("画像フォーマット:" + format)
	//サイズ表示
	point := img.Bounds().Size()
	fmt.Println("横幅=" + strconv.Itoa(point.X) + ", 縦幅=" + strconv.Itoa(point.Y))

	if format != "png" {
		log.Fatalf("対応している画像フォーマットではありません")
	}

	if decode {
		decodeText := decodeSteganography(img, point.X)
		fmt.Println(decodeText)
		return
	}

	if detailView {
		fmt.Printf("%8s %6s %6s %6s %6s\n", "[x,y=0]", "R", "G", "B", "A")
		for x := 0; x < point.X; x++ {
			r, g, b, a := img.At(x, 0).RGBA()
			fmt.Printf("%8d %6d %6d %6d %6d\n", x+1, r>>8, g>>8, b>>8, a>>8)
		}
		return
	}

	if copy {
		copyFile(img)
		return
	}

	drawSteganography(img, embeddedText)
}

func drawSteganography(oimg image.Image, text string) {

	bounds := oimg.Bounds()
	img := image.NewNRGBA(bounds)

	b := []byte(text)
	fmt.Println(b)
	fmt.Printf("input text byte %d\n", len(b))

	bc := len(b)
	var counter int
	fmt.Println("Original Data")
	fmt.Printf("%8s %6s %6s %6s %6s\n", "[x,y=0]", "R", "G", "B", "A")
	for y := 0; y < bounds.Max.Y; y++ {
		for x := 0; x < bounds.Max.X; x++ {

			if y == 0 {
				// y座標が0の場合に文字列を埋め込む、指定された文字数分だけ処理
				// 最後は0x00 0x00 0x00で終端する
				if bc >= counter || bc%3 != 0 && bc >= counter-3 {
					// fmt.Printf("%d : %d\n", bc, counter)

					or, og, ob, oa := oimg.At(x, y).RGBA()
					fmt.Printf("%8d %6d %6d %6d %6d\n", x+1, or>>8, og>>8, ob>>8, oa>>8)

					color := color.RGBA{
						R: alignment(b, bc, counter),
						G: alignment(b, bc, counter+1),
						B: alignment(b, bc, counter+2),
						A: 255, //A値は255で固定、RGB値に依存するため任意の値を入れるとRGB値が壊れる
					}
					img.Set(x, y, color)
					counter += 3

				} else {
					img.Set(x, y, oimg.At(x, y))
				}
			} else {
				img.Set(x, y, oimg.At(x, y))
			}
		}
	}

	f, err := os.Create("sg.png")
	if err != nil {
		log.Fatal(err)
	}

	if err := png.Encode(f, img); err != nil {
		f.Close()
		log.Fatal(err)
	}

	if err := f.Close(); err != nil {
		log.Fatal(err)
	}
}

func copyFile(oimg image.Image) {

	bounds := oimg.Bounds()
	img := image.NewNRGBA(bounds)

	for y := 0; y < bounds.Max.Y; y++ {
		for x := 0; x < bounds.Max.X; x++ {

			img.Set(x, y, oimg.At(x, y))

		}
	}

	f, err := os.Create("sg.png")
	if err != nil {
		log.Fatal(err)
	}

	if err := png.Encode(f, img); err != nil {
		f.Close()
		log.Fatal(err)
	}

	if err := f.Close(); err != nil {
		log.Fatal(err)
	}
}

func alignment(b []byte, len int, counter int) uint8 {

	if counter < len {
		return uint8(b[counter])
	} else {
		return 0
	}
}

func decodeSteganography(img image.Image, width int) string {
	textb := []byte{}
	fmt.Printf("%8s %6s %6s %6s %6s\n", "[x,y=0]", "R", "G", "B", "A")

	for x := 0; x < width; x++ {
		r, g, b, a := img.At(x, 0).RGBA()

		r8 := r >> 8
		g8 := g >> 8
		b8 := b >> 8

		rb := i32tob(r)
		gb := i32tob(g)
		bb := i32tob(b)
		ab := i32tob(a)

		fmt.Printf("%8d %6d %6d %6d %6d\n", x+1, rb[1], gb[1], bb[1], ab[1])

		textb = append(textb, rb[1], gb[1], bb[1])

		// 処理終端
		if r8 == 0 && g8 == 0 && b8 == 0 {
			break
		}
	}
	return string(textb)
}

func i32tob(val uint32) []byte {
	r := make([]byte, 4)
	for i := uint32(0); i < 4; i++ {
		r[i] = byte((val >> (8 * i)) & 0xff)
	}
	return r
}

めちゃハマった点

RGBAのAは任意に変更できない

元々A(透明度)にもデータを格納してRGBAにアスキー文字であれば4文字を格納しようとプログラムを組んでいましたが、書き込んだデータをデコードすると文字化け(データが崩れている)が発生し数時間格闘していましたが上記のメッセージを見て理解しました。AはRGBの値に依存するので適当な値をいれてWriteしてもうまく反映されないようです。( おそらく R=10 B=10 C=10 A=9 とかができない)

使ってみよう

ファイルをsteganography/steganography.goとする

実行ファイルビルド

go build -o ./bin/steganography steganography/steganography.go

文字の埋め込み

image/red.png に"ABCDE"文字列を埋め込む

./bin/steganography  -f image/red.png -t ABCDE
画像フォーマット:png
横幅=800, 縦幅=800
[65 66 67 68 69]
input text byte 5
Original Data
 [x,y=0]      R      G      B      A
       1    255      0      0    255
       2    255      0      0    255
       3    255      0      0    255

デコード

"ABCDE"を埋め込んだ場合の画像

./bin/steganography -f sg.png -d
画像フォーマット:png
横幅=800, 縦幅=800
 [x,y=0]      R      G      B      A
       1     65     66     67    255
       2     68     69      0    255
       3      0      0      0    255
ABCDE

"あいうえお漢字-1😋" を埋め込んだ場合の例

画像フォーマット:png
横幅=300, 縦幅=200
 [x,y=0]      R      G      B      A
       1    227    129    130    255
       2    227    129    132    255
       3    227    129    134    255
       4    227    129    136    255
       5    227    129    138    255
       6    230    188    162    255
       7    229    173    151    255
       8     45     49    240    255
       9    159    152    139    255
      10      0      0      0    255
あいうえお漢字-1😋

サンプル

元画像1

元画像1に"あいうえお漢字-1😋" を埋め込んだ画像

元画像2

元画像2に"あいうえお漢字-3😷" を埋め込んだ画像

生成された画像を見て

言われないと気づかないレベルですが左上の画像が改変されているのは目視ではわかってしまいます。これは文字列が長いためでIPアドレス程度の4バイトであれば目立たないかもしれません。

実用性を考えた改良点の案

ちょっとした改良

  • スタートポジションが[x,y]=[0,0]なのがバレバレでいけてないかつ、画像もあきらかに改変がわかってしまうのである程度スタートポジションを変更できるようにしたい
  • スタートポジションをランダムにする=>その場合はスタートポジションをファイル名やDBに書き込む必要がある
  • またはスタートポジションを規則性をもたせる、例えば12月だったら[12,0]から〜など
  • 4バイト書き込むときは、左上、右上、左下、右下を書き換えることでわかりずらくする

ビット的アプローチ

  • ビット操作にし、RGBの末尾の1bitを変更し文字を書き込む->1バイト書き込むのに8バイトのRGBが必要
  • こちらのほうがバイトは多く使うが元画像からの変化がすくない(はず)なので有効な可能性あり

ソース

https://github.com/AKB428/go-digital-watermark

Discussion