detamamoruのブログ

興味を持ったことや勉強したことに関して記事を書きます。主に低レイヤー寄りの記事を公開。

Voice changerを作りたくて(0): 何から始めたらええねん

出田 守です。 このシリーズは、ほとんど何もわからない状況からVoice changerを作る過程の記録です。

環境(私も含めて)

[私について]
算数・数学: 苦手で避けてきた(後悔中)。おそらく中学2年生で止まっている。
プログラミング: 努力中
[開発環境 - PC0]
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.39.0
RAM: 8.00GB
Editor: Vim 8.1.1 Terminal: PowerShell

voice changerを作りたくなりました

以前ある外国のhackerがある企業の社長になりすまして、情報を盗んだという事件の記事をどこかで見ました。その手口がvoice changerを使って社長の声になりすましたそうです。(どこにその記事があるのか分からなくなりました)
この時、voice changerを作ってみたくなったのです。もちろん犯罪目的ではなく、あくまでも純粋に面白そうだからです。人を笑わせたり、ちょっとしたイタズラに使えそうです(笑)

voice changerってなんや

私はvoice changerの専門家ではないので、まずはvoice changerについて調べます。
wikipediaでは以下のように説明されています。

ボイスチェンジャー (Voice changer) とは、入力された音声を加工して違う音声に聞こえるようにする機械[1]、またはその機能のついた電話機である。変声機や音声変換機とも呼ばれる。機材の他にもパソコンで使えるソフトウェア[2]のボイスチェンジャーも存在する。

Wikipedia - ボイスチェンジャー

入力された音声信号を何かしら加工して出力する機械ですね。

どのように作っていくか

voice changerの実現方法はソフトウェアとハードウェアの2種類あると思っています。
まずは、コストがかからないソフトウェアで作って、雰囲気をつかんでからハードウェアに挑戦します。
ゼロからソフトウェアで作っていくうえで、主な流れを以下に示します。

  1. 音声信号入力
  2. 音声信号をフーリエ変換
  3. 音声信号を解析
  4. 音声信号を加工
  5. 音声信号を逆フーリエ変換
  6. 音声信号出力

いっちょまえにフーリエ変換とか書いていますが、何にも分かっていません。環境の章でも書いた通り数学が苦手です。ですので、フーリエ変換を学ぶために -> 三角関数を学ぶために -> ...という再帰ループが待っていそうです。
まあ、とりあえずこのような感じでやっていきます。

レインボーテーブル入門

出田 守です。 情報セキュリティに興味を持ち、独学で勉強中です。 主にPythonを使用します。時々勉強のためにCを使用することもあります。

ゴール

レインボーテーブルについて理解したことを簡単に説明して、実際に実装してみます。

レインボーテーブルとは

あるハッシュ値から平文を得るために使われる技術です。

最も単純な方法

あるハッシュ値から平文を得るための最も単純な方法は、ハッシュ値と平文のペアから成るテーブルを作って、あるハッシュ値で検索をすれば実現できます。例えば、以下のテーブルから「password」のmd5ハッシュ値「5f4dcc3b5aa765d61d8327deb882cf99」を検索することで元の平文「password」が得られます。

ハッシュ値 平文
e10adc3949ba59abbe56e057f20f883e 123456
5f4dcc3b5aa765d61d8327deb882cf99 password
25f9e794323b453885f5181f1b624d0b 123456789
... ...

ただ、このテーブルではかなり大きなサイズになってしまいます。
そこで考えられたのがレインボーテーブルです。

レインボーテーブル作成

まず、レインボーテーブルを作成するにあたり、二つの関数を用意します。

  1. ハッシュ関数: 平文からハッシュ値を得るための関数です。
  2. 還元関数: ハッシュ値から無造作に平文を選択する関数です。ただし、同じハッシュ値なら常に同じ平文を選択する必要があります。この条件を満たしていればどのような関数でも良いようです。

次に、上記二つの関数を使って、チェインを作ります。具体的には以下のように作ります。

  1. 平文からハッシュ関数を使って、ハッシュ値を得ます。
  2. 得られたハッシュ値を還元関数を使って、別の平文を得ます。
  3. さらにその別の平文をハッシュ関数を使って、ハッシュ値を得ます。
  4. 1.~3.を決められたチェインの長さまで繰り返します。ただし、還元関数は列毎に別の平文を得られるようにします。
  5. 最後に、先頭と末尾の平文のみを残したものが一つのチェインとなります。

このチェインを決められた数まで作成したものがレインボーテーブルとなります。

ここまでを一度実装してみます。
還元関数のアイデアは以下のページを参考にしました。
d.hatena.ne.jp

また、レインボーテーブル作成時にtqdmライブラリを使って、プログレスバーを表示していますので、tqdmをインストールする必要があります。

import hashlib
import itertools
import random

from tqdm import tqdm

class RainbowTable(object):
    NUM   = "0123456789"
    LOWER = "abcdefghijklmnopqrstuvwxyz"
    UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    def __init__(self, func_hash, char_type, char_length, chain_length, n_chains):
        self.func_hash    = self.md5 if func_hash=="md5" else self.sha1
        self.char_type    = char_type
        self.chars        = {i:c for i, c in enumerate(char_type)}
        self.char_length  = char_length
        self.chain_length = chain_length
        self.n_chains     = n_chains
        self.rainbowtable = []
        if self.char_length>16:
            print("Please char_length must be 16 or less.")
            exit(1)

    def md5(self, p):
        return hashlib.md5(p.encode()).hexdigest()

    def sha1(self, p):
        return hashlib.sha1(p.encode()).hexdigest()

    def reduction(self, h, i):
        p = ""
        i = int(h, 16)+i
        while i:
            p += self.chars[i%len(self.char_type)]
            i //= len(self.char_type)
        return p[:self.char_length]

    def chain(self, p):
        ch = []
        for i in range(self.chain_length):
            ch.extend([p, self.func_hash(p)]) # plain + hash
            p     = rt.reduction(ch[-1], i) # reduction plain
        ch.append(p)
        return ch

    def rainbow_table(self, path):
        fp = open(path, "w")
        p = itertools.product(self.char_type, repeat=self.char_length) # product all patern in char_type
        for i, s in enumerate(tqdm(p), start=1):
            s = "".join(s)
            chain = self.chain(s)
            # self.rainbowtable.append([chain[0], chain[-1]]) # append only chain head and chain tail
            fp.writelines(" ".join([chain[0], chain[-1]]))
            fp.write("\n") # write format: "head_chain tail_chain\n"
            if self.n_chains and i>=self.n_chains: # continue if n_chains==0 or i<n_chains, n_chains==0 is all patern
                break
        fp.close()

    def read_table(self, path):
        fp = open(path, "r")
        self.rainbowtable = [s.strip().split(" ") for s in fp.readlines()] # read format: "head_chain tail_chain\n"
        fp.close()

if __name__ == "__main__":
    p = "00000000"

    rt = RainbowTable(func_hash="md5",
                      char_type=RainbowTable.NUM+RainbowTable.LOWER,
                      char_length=8,
                      chain_length=1000,
                      n_chains=1000)

    rt.rainbow_table("sample.rt")
    rt.read_table("sample.rt")
解読

あるハッシュ値からレインボーテーブルを使って平文を得る方法が以下になります。

  1. あるハッシュ値から末尾で使用した還元関数を使って、平文を得ます。
  2. 得られた平文とレインボーテーブルの末尾の平文が一致するか確かめます。
    1. もし、末尾と一致すれば、一致したチェインを先頭から復元し、あるハッシュ値と同じハッシュ値の位置の一つ手前が目的の平文となります。
    2. もし、一致しなければ、一つ手前で使用した還元関数を使って、あるハッシュ値から平文を得ます。さらに得られた平文をハッシュ関数を使ってハッシュ値を得ます。さらにさらに末尾の還元関数を使って、得られたハッシュ値から平文を得ます。そして、2.のチェックを行います。以降チェインの長さまで一致するまで繰り返します。

ここまでを実装してみます。

...
class RainbowTable(object):
...
    def decode(self, t):
        if not self.rainbowtable:
            return False
        for i in range(self.chain_length-1, -1, -1): # column
            p = self.reduction(t, i) # to plain
            for j in range((self.chain_length-1)-i): # loop diff from tail
                h = self.func_hash(p)
                p = self.reduction(h, i+j+1)
            matched_chain = self.match_tail(p)
            if matched_chain:
                break
        if not matched_chain:
            return False
        matched_chain = self.chain(matched_chain[0])
        return matched_chain[matched_chain.index(t)-1]
...
if __name__ == "__main__":
...
    print("decoded =", rt.decode(rt.md5(p)))

githubに公開

github.com

Steganography(ステガノグラフィ)入門

出田 守です。 情報セキュリティに興味を持ち、独学で勉強中です。 主にPythonを使用します。時々勉強のためにCを使用することもあります。

目的

CTFの問題を解いていると、Steganographyに出会いました。名前は聞いたことあるけど、詳しくは知らなかったので調べました。調べた内容の説明後、実装までが今回の目的です。

Steganographyとは

Steganographyとはあるデータを他のデータに埋め込み秘匿してしまう技術の一種です。埋め込まれるデータをvesselやdummyと呼ばれるそうです。データとしては画像の他、音声データ、動画データなども対象になるようです。 従来のSteganography技術には以下があるようです。

  1. LSBステガノグラフィ
  2. 特定周波数ステガノグラフィ
  3. サンプリング誤差ステガノグラフィ

ただし、これらの技術のデータ埋め込み容量は小さいそうです。一方BPCS-Steganographyではデータ埋め込み容量は約50%にもなるようです。 今回は特に画像を対象にBPCS-Steganographyについて学んでみます。

参照: BPCS-Steganography の原理

BPCS-Steganography

BPCS-SteganographyはSteganography技術の一種です。ビットプレーン分解と人間の視覚情報認識能力を利用した手法のようです。

ビットプレーン分解

1情報のうちに複数のビット情報を持つものを1ビット毎に分解すること。例えば24色画像の場合1ピクセルあたり224色あります。224色の内訳はRED、GREEN、BLUEそれぞれ28色で構成されています。つまり282828色ということです。ビットプレーンは抽出した特定のビットのことを言います。ここで例えばREDの28の最上位ビットをMSBといい、最下位ビットをLSBといいます。
ここでビットプレーン分解を行うプログラムを実装してみます。

import numpy as np

from PIL import Image

def read_image_as_numpy(path):
    img = Image.open(path) # read img
    arr = np.array(img)    # convert to numpy array
return arr

def to_binary(arr):
    if len(arr.shape)==3: # 3 channels (R, G, B)
        bits = np.unpackbits(arr, axis=2) # [[[128,128,128]...]...] => [[[100000001000000010000000]...]...]
        bits = bits.reshape(bits.shape[:2]+(arr.shape[2], 8,)) # divide each color (h, w, c*8) => (h, w, c, 8)
        return bits
    elif len(arr.shape)==2: # 2 channels gray-scale
        bits = np.unpackbits(arr, axis=1)
        bits = bits.reshape(bits.shape[:1]+(arr.shape[1], 8,)) # (h, w*8) => (h, w, 8)
        return bits
    elif len(arr.shape)==1:
        org = arr.shape             # original shape
        arr = np.unpackbits(arr)
        arr = arr.reshape(org+(8,)) # (original shape*bits) => (original shape, 8)
        return arr
    else:
        print("Unsupported shape of image")
        exit(1)
    return None

def to_image(arr):
    if len(arr.shape)==4: # for 3 channels (R, G, B)
        arr = arr.reshape(arr.shape[:2]+(arr.shape[2]*arr.shape[3],)) # (h, w, c, 8) => (h, w, c*8)
        arr = np.packbits(arr, axis=2)
        return arr
    elif len(arr.shape)==3: # for 2 channels gray-scale
        arr = arr.reshape(arr.shape[:1]+(arr.shape[1]*arr.shape[2],)) # (h, w, 8) => (h, w*8)
        arr = np.packbits(arr, axis=1)
        return arr
    else:
        print("Unsupported shape of image")
        exit(1)
    return None

def extract_bitplane(arr, color, bit):
    if len(arr.shape)==4: # RGB
        bitplane = arr[:,:,color,bit] # bitplane shape is (h, w) 2d array
    elif len(arr.shape)==3: # gray-scale
        bitplane = arr[:,:,bit] # bitplane shape is (h, w) 2d array
    else:
        print("Unsupported shape of image")
        exit(1)
    return bitplane
import sys
import numpy as np
import bpcs  as bp

from PIL import Image

if len(sys.argv)<4:
    print("USAGE: {0} <PATH> <COLOR> <BIT>".format(sys.argv[0]))
    print("    PATH:  image path")
    print("    COLOR: GRAY=-1, RED=0, GREEN=1, BLUE=2")
    print("    BIT  : 0~7 (0:MSB, 7:LSB)")
    exit(1)
    
PATH  = sys.argv[1]
COLOR = int(sys.argv[2])
BIT   = int(sys.argv[3])

def merge_bitplane_to_image(bitplane, arr, color):
    arr = bp.to_image(arr)
    img = np.zeros(arr.shape)
    img[:,:,color] = bitplane
    return img

arr = bp.read_image_as_numpy(PATH)
if len(arr.shape)<2 or len(arr.shape)>3:
    print("Unsupported shape of image")
    exit(1)
arr = bp.to_binary(arr) # arr.shape = (h, w, 3(color), 8(byte)) or (h, w, 8(byte))
# arr = bp.to_image(arr)  # arr.shape = (h, w, 3) or (h, w)
bitplane = bp.extract_bitplane(arr, COLOR, BIT)
bitplane[bitplane>0] = 255
if COLOR!=-1 and len(arr.shape)==4:
    arr = merge_bitplane_to_image(bitplane, arr, COLOR)
else:
    arr = bitplane
Image.fromarray(np.uint8(arr)).show()           # show image
# Image.fromarray(np.uint8(arr)).save("test.png") # save image
$ python show_specific_bits.py sample.jpg 0 7 

第一引数に画像パス、第二引数に抽出カラー、第三引数に抽出ビットです。

結果が以下になりました。

f:id:detamamoru:20181214111709p:plain
下位から7bit目
f:id:detamamoru:20181214111838p:plain
下位から4bit目
f:id:detamamoru:20181214111928p:plain
下位から0bit目

下位ビットにいくにつれて元画像が分からなくなっていますね。

視覚情報認識能力

上記のビットプレーン分解を見ると、LSBよりMSBのほうが、より元画像に近く見えます。このことは人間にとって重要な情報はMSBに近いビットさえあれば元画像として認識可能ということです。

PBCとCGC

画像にデータを埋め込む際、PBC(Pure Binary Code)よりもCGC(Canonical Gray Code)の方が埋め込み後の画像の変化が少ないそうです。
PBCからCGCへの変換は以下の参照ページを参照してください。
では、PBCからCGCとCGCからPBCの変換を行うプログラムを実装してみます。

...
def pbc_to_cgc(arr):
    """
    ref:http://datahide.org/BPCSj/pbc-vs-cgc-j.html
    """
    if len(arr.shape)==3 and arr.shape[2]==3:
        print("Please first convert to binary. i.e. arr = to_binary(arr)")
        exit(1)
    cgc = arr.copy()
    if len(cgc.shape)==4: # RGB
        for i in range(1, 8):
            cgc[:,:,:,i] = np.logical_xor(arr[:,:,:,i-1], arr[:,:,:,i]) # gi = bi-1^bi
    elif len(cgc.shape)==3: # gray-scale
        for i in range(1, 8):
            cgc[:,:,i] = np.logical_xor(arr[:,:,i-1], arr[:,:,i]) # gi = bi-1^bi
    else:
        print("Unsupported shape of image")
        exit(1)
    return cgc

def cgc_to_pbc(arr):
    """
    ref:http://datahide.org/BPCSj/pbc-vs-cgc-j.html
    """
    if len(arr.shape)==3 and arr.shape[2]==3:
        print("Please first convert to binary. i.e. arr = to_binary(arr)")
        exit(1)
    pbc = arr.copy()
    if len(pbc.shape)==4: # RGB
        for i in range(1, 8):
            pbc[:,:,:,i] = np.logical_xor(arr[:,:,:,i], pbc[:,:,:,i-1]) # bi = gi^bi-1
    elif len(pbc.shape)==3: # gray-scale
        for i in range(1, 8):
            pbc[:,:,i] = np.logical_xor(arr[:,:,i], pbc[:,:,i-1]) # bi = gi^bi-1
    else:
        print("Unsupported shape of image")
        exit(1)
    return pbc
import sys
import numpy as np
import bpcs  as bp

from PIL import Image

if len(sys.argv)<2:
    print("USAGE: {0} <PATH>".format(sys.argv[0]))
    print("    PATH:  image path")
    exit(1)
    
PATH  = sys.argv[1]

arr = bp.read_image_as_numpy(PATH)
if len(arr.shape)<2 or len(arr.shape)>3:
    print("Unsupported shape of image")
    exit(1)
arr = bp.to_binary(arr)
arr = bp.pbc_to_cgc(arr)
# arr = bp.cgc_to_pbc(arr)
arr = bp.to_image(arr)
Image.fromarray(np.uint8(arr)).show()             # show image
# Image.fromarray(np.uint8(arr)).save("test.png") # save image

試した結果が以下です。味のある感じになりましたね笑

f:id:detamamoru:20181224102752p:plain
pbc->cgc
ちなみに元画像と同じですがcgcからpbcにした結果も載せておきます。
f:id:detamamoru:20181224103017p:plain
cgc->pbc

参照: Two Binary Number Coding Systems

複雑さ

埋め込まれる画像が埋め込まれる対象として適しているかを示す尺度のようです。下記参照ページでは白黒の境界の合計値にその画像での最大境界数を割った割合を複雑さとしています。この複雑さは画像全体としても局所的にも適用できます。まずは、画像全体のそれぞれのbitplaneでの複雑さを出力してみます。

...
def get_block_as_iter(bitplane, blocksize):
    n_rows, n_cols = bitplane.shape
    for y in range(0, n_rows, blocksize[0]):
        for x in range(0, n_cols, blocksize[1]):
            block = bitplane[y:y+blocksize[0], x:x+blocksize[1]]
            if block.shape==blocksize:
                yield x, y, block
    return None

def complexity(bitplane):
    n_rows, n_cols = bitplane.shape # bitplane shape is (h, w) 2d array
    max_cpx        = ((n_rows-1)*n_cols)+((n_cols-1)*n_rows) # max complexity
    k              = 0
    xor  = np.logical_xor(bitplane[1:], bitplane[:-1])       # row complexity
    k   += len(xor[xor==True])
    xor  = np.logical_xor(bitplane[:,1:], bitplane[:,:-1])   # col complexity
    k   += len(xor[xor==True])
    return k/max_cpx*1.0
import sys
import numpy as np
import bpcs  as bp

from PIL import Image

if len(sys.argv)<4:
    print("USAGE: {0} <PATH> <COLOR> <BIT>".format(sys.argv[0]))
    print("    PATH:  image path")
    print("    COLOR: GRAY=-1, RED=0, GREEN=1, BLUE=2")
    print("    BIT  : 0~7 (0:MSB, 7:LSB)")
    exit(1)

PATH  = sys.argv[1]
COLOR = int(sys.argv[2])
BIT   = int(sys.argv[3])

arr = bp.read_image_as_numpy(PATH)
if len(arr.shape)<2 or len(arr.shape)>3:
    print("Unsupported shape of image")
    exit(1)
# arr = bp.pbc_to_cgc(arr)
arr = bp.to_binary(arr)

# image complexity
bitplane = bp.extract_bitplane(arr, COLOR, BIT) # extract bitplane as binary, shape is (h, w)
cpx      = bp.complexity(bitplane)
print("image complexity =", cpx)

# block complexity
for i, (x, y, block) in enumerate(bp.get_block_as_iter(bitplane, (8,8))):
    cpx = bp.complexity(block)
    print("block[{0}] complexity =".format(i), cpx)

MSBに近いと複雑さはないです。LSBは0.5付近の複雑さを多く持っています。

埋込みデータ

秘密データは決められたサイズに小ブロック化します。もしそのブロックの複雑さが閾値以下なら、白黒データ(1から始まる縞々データ)とXORさせて複雑にします。秘密データ抽出のために、そのブロックをXORしたかどうかをコンジュゲーションマップとしてどこかに記録しておく必要があります。ということは埋め込む際に何かしらフォーマットを決めておくと良さそうです。

ではまず、秘密データを小ブロック化して、白黒データをXORするプログラムを実装してみます。なお、今回は秘密データとしてASCII文字列メッセージを対象とします。

I am hiding. Please do not search.\n
...
def padding(arr, blocksize):
    block_row,block_col = blocksize
    n_row = len(arr)
    lack_row = block_row-n_row%block_row
    if lack_row>0:
        wb = create_wb((lack_row, 8)) # ascii only,so 8bit. wb is [[1,0,1,0,1,0,1,0]*lack_row]
        arr = np.concatenate((arr, np.packbits(wb))) # concatenate arr and packed wb
    return arr

def read_message_as_numpy(path, blocksize):
    fp  = open(path, "r")
    msg = fp.read()
    msg = np.array([ord(c) for c in msg], dtype=np.uint8)
    msg = np.concatenate((msg, SHINOBI))
    msg = padding(msg, blocksize)
    fp.close()
    return msg

def create_wb(size):
    if size[1]==1:
        return np.array([[1]*size[0]])
    wb = np.tile([1, 0], (size[0],size[1]))[:size[0], :size[1]]
    if len(wb[1::2])>0:
        wb[1::2] = 1-wb[1::2] # invert only odd rows
    return wb

def complication(block):
    wb = create_wb(block.shape)
    return np.uint8(np.logical_xor(block, wb))
import sys
import numpy as np
import bpcs  as bp

from PIL import Image

if len(sys.argv)<2:
    print("USAGE: {0} <PATH>".format(sys.argv[0]))
    print("    PATH:  secret file path")
    exit(1)
    
PATH = sys.argv[1]

blocksize = (8,8)
ath       = 0.45 # complexity threshold

arr  = bp.read_message_as_numpy(PATH, blocksize)
arr  = bp.to_binary(arr)
for x, y, block in bp.get_block_as_iter(arr, blocksize):
    cpx = bp.complexity(block)
    print(block)
    print(block.shape)
    print("original complexity  =", cpx)
    if cpx<ath:
        block = bp.complication(block)
        print("converted complexity =", bp.complexity(block))
    print("="*30)

秘密データの終わりに特定の文字を仕込みました。
各ブロックの複雑さを見てみると、0.45以下のものを白黒データでXORしてあげると、複雑さが増しているのが分かります。

データ埋込み

いよいよデータを埋め込む準備ができました。 埋め込みはREDのLSBのビットプレーンから順番に各ブロックを走査し、複雑さが閾値以上のブロックと秘密データのブロックを入れ替えます。
なお、埋め込み後の画像出力はPNGです。JPGだと埋め込んだデータが壊れていためです。おそらく原因は非可逆圧縮だからでしょうか。
コンジュゲーションマップは秘密データの各ブロックがXORしたかを0または1で表すことにしました。これの埋め込みは秘密データの各ブロックの埋め込み前に埋め込まれるデータブロックの複雑さに関係なく埋め込まれます。注意として、コンジュゲーションマップは複雑さが閾値以下でも複雑化していません。

def secret_blocks(arr, blocksize, ath):
    # create secret blocks
    secret_blocks = []
    conj_map      = np.array([0]*int(arr.shape[0]/arr.shape[1]), dtype=np.uint8)
    for i, (x, y, block) in enumerate(get_block_as_iter(arr, blocksize)):
        cpx = complexity(block)
        if cpx<ath:
            block       = complication(block)
            conj_map[i] = 1
        secret_blocks.append(block)
    # create conjugation map (NOTE: not complication)
    conj_map = conjugation_map(conj_map, blocksize)
    return secret_blocks, conj_map

def encode(arr, secret_blocks, conj_map, blocksize, ath):
    COLOR = 1
    if len(arr.shape)==4: # RGB
        COLOR = 3
    elif len(arr.shape)==3: # gray-scale
        arr = arr.reshape(arr.shape[:2]+(1, arr.shape[2],))
        COLOR = 1
    else:
        print("Unsupported shape of image")
        exit(1)
    secret_blocks = secret_blocks[:]
    conj_map      = conj_map[:]
    for color in range(COLOR):
        for bit in (7, -1, -1):
            bitplane = extract_bitplane(arr, color, bit)
            for i, (x, y, block) in enumerate(get_block_as_iter(bitplane, blocksize)):
                if conj_map:
                    bitplane[y:y+blocksize[0], x:x+blocksize[1]] = conj_map.pop(0)
                elif complexity(block)>ath:
                    bitplane[y:y+blocksize[0], x:x+blocksize[1]] = secret_blocks.pop(0)
                if not secret_blocks:
                    break
            arr[:,:,color,bit] = bitplane
            if not secret_blocks:
                if arr.shape[2]==1: # gray-scale
                    arr = arr.reshape(arr.shape[:2]+(arr.shape[3],))
                return arr
    return None
import sys
import numpy as np
import bpcs  as bp

from PIL import Image

if len(sys.argv)<3:
    print("USAGE: {0} <IPATH> <SPATH>".format(sys.argv[0]))
    print("    IPATH:  image path")
    print("    SPATH:  secret file path")
    exit(1)
    
IPATH = sys.argv[1]
SPATH = sys.argv[2]

blocksize = (8,8)
ath       = 0.45 # complexity threshold

# prepare secret blocks
arr = bp.read_message_as_numpy(SPATH, blocksize)
arr = bp.to_binary(arr)
secret_blocks, conj_map = bp.secret_blocks(arr, blocksize, ath)

# encode
arr = bp.read_image_as_numpy(IPATH)
arr = bp.to_binary(arr)
arr = bp.encode(arr, secret_blocks, conj_map, blocksize, ath)
arr = bp.to_image(arr)
Image.fromarray(np.uint8(arr)).show()
Image.fromarray(np.uint8(arr)).save("images/encoded.png")

元画像が以下です。

f:id:detamamoru:20181227193004j:plain
元画像
そして、埋込み後の結果は以下です。
f:id:detamamoru:20181228104333p:plain
埋込み後
全く違いが分かりませんね!
ついでに、MSBのビットプレーンにも埋め込んでみました。
f:id:detamamoru:20181228104402p:plain
MSB埋込み後
帽子付近にちょこちょこっとノイズが入っていますね。

データ抽出

一応埋め込めましたが、実際に取り出せないとただの落書きになってしまうので、埋め込んだデータを抽出してみます。

...
def decode_block(block):
    decoded = np.packbits(block).tolist()
    decoded = "".join([chr(c) for c in decoded])
    return decoded

def decode(arr, blocksize, ath):
    COLOR = 1
    if len(arr.shape)==4: # RGB
        COLOR = 3
    elif len(arr.shape)==3: # gray-scale
        arr = arr.reshape(arr.shape[:2]+(1, arr.shape[2],))
        COLOR = 1
    else:
        print("Unsupported shape of image")
        exit(1)
    conj     = True
    conj_map = []
    decoded  = ""
    shinobi  = "".join([chr(c) for c in SHINOBI])
    tail     = "".join([chr(c) for c in TAIL])
    for color in range(COLOR):
        for bit in (7, -1, -1):
            bitplane = extract_bitplane(arr, color, bit)
            for x, y, block in get_block_as_iter(bitplane, blocksize):
                if conj:
                    decoded += decode_block(block)
                    if tail in decoded:
                        conj_map = [ord(c) for c in decoded[:decoded.index(tail)]]
                        conj     = False
                        decoded  = ""
                elif complexity(block)>ath:
                    if conj_map and conj_map.pop(0):
                        decoded += decode_block(complication(block))
                    else:
                        decoded += decode_block(block)
                    if shinobi in decoded:
                        return decoded[:decoded.index(shinobi)]
    return None
import sys
import numpy as np
import bpcs  as bp

from PIL import Image

if len(sys.argv)<2:
    print("USAGE: {0} <PATH>".format(sys.argv[0]))
    print("    PATH:  encoded image path")
    exit(1)
    
PATH = sys.argv[1]

blocksize = (8,8)
ath       = 0.45 # complexity threshold

arr = bp.read_image_as_numpy(PATH)
arr = bp.to_binary(arr)
decoded = bp.decode(arr, blocksize, ath)
print("[decoded code]")
print(decoded)

githubに公開

完全版を公開しました。

github.com

参照: http://www.datahide.org/BPCSe/Articles/Ref-6.SPIE98.pdf