Ubuntu Weekly Recipe

第602回2020年になったのでテキストに半角スペースで暗号文を埋め込もう

2020年が始まりました。⁠2020」という数字列を見ると何か見えてきませんか。そう、半角スペースですね。そこで今回は2020年にちなんで、テキストファイルに半角スペースを用いて暗号文を埋め込む方法を紹介しましょう。

テキストファイルにメッセージを埋め込める「stegsnow」

半角スペースはASCIIコードで「0x20」となります[1]⁠。UTF-8な文化圏で生活している一般的なユーザーであれば、適当なファイルやストレージをバイナリダンプした際に、適度な間隔で「0x20」が登場するデータを見ることで「ここはなんか英文っぽいな」と判断することがよくあるでしょう[2]⁠。hdコマンドやhexdumpコマンドを使う場合はASCIIの印字可能な文字もセットでダンプするので、英文ぐらいなら一発でわかるのですが、そういうことができないケースもあるのです。

結果として「0x20」もしくはプレフィックスを省いた「20」の登場に過敏になります。そして「2020」という数字列を見て、⁠半角スペースが連続している!」と妄言を垂れ流すようになるのです。こいつはもうダメだ。

さて、半角スペース(ホワイトスペース)を含む空白文字は印字可能でありながら人類には認識できない文字として、古来より便利に使われてきました。たとえば英語ではわかち書きとして単語の区切り文字に使われていますし、日本でも闕字のように敬意を表すための用法として空白文字を挿入することがあります。人類の高位存在が備えているという特殊スキル「行間を読む」も一種の空白文字認識技法ではないかと考えられています。

コンピューターの世界でも、半角スペースやタブ文字、改行文字などの空白文字を組み合わせて記述できるプログラミング言語Whitespaceなんてものも存在します。

今回紹介する「stegsnow」はこんな「空白文字を活用したツール」の一種です。プレーンテキストの「行末」「半角スペース」「タブ文字」からなるエンコードされた文字列を挿入することで任意のデータを埋め込めます。

インストールは単にリポジトリからパッケージをインストールするだけです。

$ sudo apt install stegsnow

stegsnowでデータを埋め込む

まずはあらかじめ埋め込み対象のプレーンテキストを用意しておきます。これは任意の行長で改行されたテキストが想定されています。埋め込んだデータをやり取りすることから、メールの本文などをイメージすれば良いでしょう。

stegsnow自身は、元のテキストの行末に半角スペースで構築されたデータを追加します。特に指定しなければ各行が80文字以内に収まるようにデータを埋め込むようです[3]⁠。ちなみに「80文字」の部分は-lオプションで変更可能です。

元データから埋め込みデータへのエンコードはタブ文字で区切られた最大7個の半角スペースによって実現しています。つまりタブ文字とタブ文字の間の半角スペースの数で、データを表すのです。結果的に個々のタブ文字の間に3bitのデータを埋め込めることになります。ASCII文字だと7bitのデータが必要なので、3つのデータ列に分割されるというわけですね。

さらに前述したとおり、最大行長も決まっているため、プレーンテキストごとに埋め込めるデータのサイズは異なります。1行あたりが短く行数が多いテキストならデータ容量は大きくなり、1行あたりが長く行数が少ないテキストならデータ容量は小さくなります。

今回は例として、青空文庫の銀河鉄道の夜から冒頭部分を段落区切りに空行をはさみ、行長を半角70文字におさえたテキストファイルを「body.txt」として用意しておきます。

「ではみなさんは、そういうふうに川だと言われたり、乳の流れたあとだと言
われたりしていた、このぼんやりと白いものがほんとうは何かご承知ですか」
先生は、黒板につるした大きな黒い星座の図の、上から下へ白くけぶった銀河
帯のようなところを指しながら、みんなに問いをかけました。

 カムパネルラが手をあげました。それから四、五人手をあげました。ジョバ
ンニも手をあげようとして、急いでそのままやめました。たしかにあれがみん
な星だと、いつか雑誌で読んだのでしたが、このごろはジョバンニはまるで毎
日教室でもねむく、本を読むひまも読む本もないので、なんだかどんなことも
よくわからないという気持ちがするのでした。

 ところが先生は早くもそれを見つけたのでした。

「ジョバンニさん。あなたはわかっているのでしょう」

 ジョバンニは勢いよく立ちあがりましたが、立ってみるともうはっきりとそ
れを答えることができないのでした。ザネリが前の席からふりかえって、ジョ
バンニを見てくすっとわらいました。ジョバンニはもうどぎまぎしてまっ赤に
なってしまいました。先生がまた言いました。

「大きな望遠鏡で銀河をよっく調べると銀河はだいたい何でしょう」

 やっぱり星だとジョバンニは思いましたが、こんどもすぐに答えることがで
きませんでした。

 先生はしばらく困ったようすでしたが、眼をカムパネルラの方へ向けて、

「ではカムパネルラさん」と名指しました。

 するとあんなに元気に手をあげたカムパネルラが、やはりもじもじ立ち上が
ったままやはり答えができませんでした。

このテキストに「けれども本当の幸いは一体なんだろう」というデータを埋め込んでみましょう。

$ stegsnow -m "けれども本当の幸いは一体なんだろう" body.txt output.txt
Message exceeded available space by approximately 29.52%.
An extra 4 lines were added.

-mでは埋め込むデータを指定し、その他の引数として埋め込む対象のテキストファイル名と、埋め込んだあとのデータを保存するテキストファイル名を指定します。それぞれ省略することで標準入力からテキストを受け取り、標準出力にテキストファイルを生成することも可能です。

最後のメッセージではデータが入り切らなかったので、4行余分に追加した、と記録されています。

実際に生成されたテキストファイルを開いてみると次のように表示されます。

図1 一部の行末に半角スペースとタブ文字が追加されている
画像

タブは>...で、半角スペースは赤背景の空白で表示しています。また右の縦のラインは80文字の部分です。

上記を見る限り、日本語が含まれる半角70文字幅の行の後ろには何もデータが入っていませんね。これはstegsnowが「1バイト=1文字」と換算していることによる弊害です。日本語テキストで半角70文字分の幅は、UTF-8だとおよそ105バイト前後になってしまいます。空行と一部の短い行以外は80バイトを超えているために、stegsnowは空行と一部の短い行にしかデータを追加できないと判断してしまっているのです。stegsnowがデータを追加できる行が限られている結果、すべてのデータを埋め込むためにファイル末尾に空行を追加する必要があったというわけです。

たとえば-Sオプションを付けることで、そのテキストファイルに埋め込めるデータ容量を確認できます。

$ stegsnow -S body.txt
File has storage capacity of between 317 and 335 bits.
Approximately 40 bytes.

今回のファイルだとたかだが40バイト程度であり、それ以上だと空行を追加しなくてはなりません。

最大行長を指定する-lオプションを利用すると、この制限を変えられます。たとえば半角70文字分に半角10文字分のデータを埋め込むとしたらおおよそ115バイト前後になります。というわけで、次のように実行してみましょう。

$ stegsnow -l 120 -m "けれども本当の幸いは一体なんだろう" body.txt output.txt
Message used approximately 63.75% of available space.

今度は行の追加が必要なかったようですね。

図2 日本語の行の末尾にもデータが追加されている
画像

今回は右の縦のラインは120文字の部分です。右端の位置がまちまちですが、これはUTF-8が可変長であるためです。たとえば1行目は日本語の部分が106バイトになっています。よって末尾の容量は14バイトとなります。stegsnowの1データあたりの最大長はタブ1文字+半角スペース7文字の8バイトなので、1データしか入らないというわけです。

さて次は出力されたファイルから、埋め込まれたデータを取り出してみましょう。これはstegsnowコマンドにファイルを渡すだけです。

$ stegsnow output.txt
けれども本当の幸いは一体なんだろう

日本語のデータもきちんと取り出せましたね。ちなみに上記のような-mオプションの使い方だと、末尾に改行は含まれないということだけ注意しておいてください。改行を含むデータを渡したい場合は、-m メッセージではなく-f ファイル名のように別ファイルにデータを保存した上でそれを渡すと良いでしょう。

さらにファイル名を指定すると、標準出力ではなくファイルに取り出したデータを保存します。バイナリデータを埋め込みたい時に使用してください。

$ stegsnow output.txt message.txt
$ cat message.txt
けれども本当の幸いは一体なんだろう

データを圧縮して埋め込む

さて、stegsnowのエンコード方式は3bitのデータを8bitのデータ(タブ文字と半角スペース)に変換します。つまりデータとしては3倍近くになるわけです。これでは効率が悪いので、埋め込むデータを圧縮したいところですね。

stegsnowは-Cオプションを付けることで、ハフマン符号でデータを圧縮・展開できます。

$ stegsnow -C -m "けれども本当の幸いは一体なんだろう" body.txt output.txt
Compressed by 4521260802379791872.00%
Message exceeded available space by approximately 240.00%.
An extra 26 lines were added.

圧縮率がおかしいことになっていますね。さらに圧縮前に比べると追加される空行も増えています。実はstegsnowは英文に最適化された静的なハフマンテーブルを利用して圧縮します。このため、英文以外だとあまり効率よく圧縮はできないようです。stegsnowのマニュアルでも、テキストデータでない場合やデータサイズが大きくなる場合は、組み込みの圧縮オプションを使用せず、あらかじめgzipなどで圧縮したデータを-fオプションで渡す方法を推奨しています。

ちなみに英文を埋め込んだ場合だとどうなるのでしょう。

未圧縮の場合:
$ stegsnow -m "Night on the Galactic Railroad" body.txt output.txt
Message used approximately 84.21% of available space.

圧縮した場合:
$ stegsnow -C -m "Night on the Galactic Railroad" body.txt output.txt
Compressed by 40.00%
Message used approximately 48.00% of available space.

圧縮が効いた結果、データ容量の84.21%を使っていたものが48.00%まで下がっています。

圧縮データが含まれたテキストファイルをデコードする場合も-Cオプションを付けてください。そうしないと「圧縮されていない」と判断し、デコードするため、意図しない結果となります。

$ stegsnow -C output.txt
Night on the Galactic Railroad

データを暗号化して埋め込む

stegsnowはICE暗号化アルゴリズムを利用したデータの暗号化にも対応しています。-pオプションを付けると任意のパスワードを指定可能で、そのパスワードを知っている人だけがデコードできるようになるのです。

$ stegsnow -l 120 -m "けれども本当の幸いは一体なんだろう" -p "パスワード" body.txt output.txt
Message used approximately 63.75% of available space.

まずはパスワード無しでデコードしてみましょう。

$ stegsnow output.txt
.~6AZnJY7
         y"ҪxZC4O&1k^6

うまくデコードできず意味不明の文字列になってしまいましたね。

次はパスワードを付けてデコードします。

$ stegsnow -p "パスワード" output.txt
けれども本当の幸いは一体なんだろう

今度は無事にデコードできました。

決して強度のある暗号化アルゴリズムではないので、あくまでカジュアルに暗号化できます、程度に考えておきましょう。

おすすめ記事

記事・ニュース一覧