2020年が始まりました。「2020」という数字列を見ると何か見えてきませんか。そう、半角スペースですね。そこで今回は2020年にちなんで、テキストファイルに半角スペースを用いて暗号文を埋め込む方法を紹介しましょう。
テキストファイルにメッセージを埋め込める「stegsnow」
半角スペースはASCIIコードで「0x20」となります[1]。UTF-8な文化圏で生活している一般的なユーザーであれば、適当なファイルやストレージをバイナリダンプした際に、適度な間隔で「0x20」が登場するデータを見ることで「ここはなんか英文っぽいな」と判断することがよくあるでしょう[2]。hdコマンドやhexdumpコマンドを使う場合はASCIIの印字可能な文字もセットでダンプするので、英文ぐらいなら一発でわかるのですが、そういうことができないケースもあるのです。
結果として「0x20」もしくはプレフィックスを省いた「20」の登場に過敏になります。そして「2020」という数字列を見て、「半角スペースが連続している!」と妄言を垂れ流すようになるのです。こいつはもうダメだ。
さて、半角スペース(ホワイトスペース)を含む空白文字は印字可能でありながら人類には認識できない文字として、古来より便利に使われてきました。たとえば英語ではわかち書きとして単語の区切り文字に使われていますし、日本でも「闕字」のように敬意を表すための用法として空白文字を挿入することがあります。人類の高位存在が備えているという特殊スキル「行間を読む」も一種の空白文字認識技法ではないかと考えられています。
コンピューターの世界でも、半角スペースやタブ文字、改行文字などの空白文字を組み合わせて記述できるプログラミング言語「Whitespace」なんてものも存在します。
今回紹介する「stegsnow」はこんな「空白文字を活用したツール」の一種です。プレーンテキストの「行末」に「半角スペース」と「タブ文字」からなるエンコードされた文字列を挿入することで任意のデータを埋め込めます。
インストールは単にリポジトリからパッケージをインストールするだけです。
stegsnowでデータを埋め込む
まずはあらかじめ埋め込み対象のプレーンテキストを用意しておきます。これは任意の行長で改行されたテキストが想定されています。埋め込んだデータをやり取りすることから、メールの本文などをイメージすれば良いでしょう。
stegsnow自身は、元のテキストの行末に半角スペースで構築されたデータを追加します。特に指定しなければ各行が80文字以内に収まるようにデータを埋め込むようです[3]。ちなみに「80文字」の部分は「-l
」オプションで変更可能です。
元データから埋め込みデータへのエンコードはタブ文字で区切られた最大7個の半角スペースによって実現しています。つまりタブ文字とタブ文字の間の半角スペースの数で、データを表すのです。結果的に個々のタブ文字の間に3bitのデータを埋め込めることになります。ASCII文字だと7bitのデータが必要なので、3つのデータ列に分割されるというわけですね。
さらに前述したとおり、最大行長も決まっているため、プレーンテキストごとに埋め込めるデータのサイズは異なります。1行あたりが短く行数が多いテキストならデータ容量は大きくなり、1行あたりが長く行数が少ないテキストならデータ容量は小さくなります。
今回は例として、青空文庫の「銀河鉄道の夜」から冒頭部分を段落区切りに空行をはさみ、行長を半角70文字におさえたテキストファイルを「body.txt」として用意しておきます。
このテキストに「けれども本当の幸いは一体なんだろう」というデータを埋め込んでみましょう。
「-m
」では埋め込むデータを指定し、その他の引数として埋め込む対象のテキストファイル名と、埋め込んだあとのデータを保存するテキストファイル名を指定します。それぞれ省略することで標準入力からテキストを受け取り、標準出力にテキストファイルを生成することも可能です。
最後のメッセージではデータが入り切らなかったので、4行余分に追加した、と記録されています。
実際に生成されたテキストファイルを開いてみると次のように表示されます。
タブは「>...
」で、半角スペースは赤背景の空白で表示しています。また右の縦のラインは80文字の部分です。
上記を見る限り、日本語が含まれる半角70文字幅の行の後ろには何もデータが入っていませんね。これはstegsnowが「1バイト=1文字」と換算していることによる弊害です。日本語テキストで半角70文字分の幅は、UTF-8だとおよそ105バイト前後になってしまいます。空行と一部の短い行以外は80バイトを超えているために、stegsnowは空行と一部の短い行にしかデータを追加できないと判断してしまっているのです。stegsnowがデータを追加できる行が限られている結果、すべてのデータを埋め込むためにファイル末尾に空行を追加する必要があったというわけです。
たとえば「-S
」オプションを付けることで、そのテキストファイルに埋め込めるデータ容量を確認できます。
今回のファイルだとたかだが40バイト程度であり、それ以上だと空行を追加しなくてはなりません。
最大行長を指定する「-l
」オプションを利用すると、この制限を変えられます。たとえば半角70文字分に半角10文字分のデータを埋め込むとしたらおおよそ115バイト前後になります。というわけで、次のように実行してみましょう。
今度は行の追加が必要なかったようですね。
今回は右の縦のラインは120文字の部分です。右端の位置がまちまちですが、これはUTF-8が可変長であるためです。たとえば1行目は日本語の部分が106バイトになっています。よって末尾の容量は14バイトとなります。stegsnowの1データあたりの最大長はタブ1文字+半角スペース7文字の8バイトなので、1データしか入らないというわけです。
さて次は出力されたファイルから、埋め込まれたデータを取り出してみましょう。これはstegsnowコマンドにファイルを渡すだけです。
日本語のデータもきちんと取り出せましたね。ちなみに上記のような「-m
」オプションの使い方だと、末尾に改行は含まれないということだけ注意しておいてください。改行を含むデータを渡したい場合は、「-m メッセージ
」ではなく「-f ファイル名
」のように別ファイルにデータを保存した上でそれを渡すと良いでしょう。
さらにファイル名を指定すると、標準出力ではなくファイルに取り出したデータを保存します。バイナリデータを埋め込みたい時に使用してください。
データを圧縮して埋め込む
さて、stegsnowのエンコード方式は3bitのデータを8bitのデータ(タブ文字と半角スペース)に変換します。つまりデータとしては3倍近くになるわけです。これでは効率が悪いので、埋め込むデータを圧縮したいところですね。
stegsnowは「-C
」オプションを付けることで、ハフマン符号でデータを圧縮・展開できます。
圧縮率がおかしいことになっていますね。さらに圧縮前に比べると追加される空行も増えています。実はstegsnowは英文に最適化された静的なハフマンテーブルを利用して圧縮します。このため、英文以外だとあまり効率よく圧縮はできないようです。stegsnowのマニュアルでも、テキストデータでない場合やデータサイズが大きくなる場合は、組み込みの圧縮オプションを使用せず、あらかじめgzipなどで圧縮したデータを「-f
」オプションで渡す方法を推奨しています。
ちなみに英文を埋め込んだ場合だとどうなるのでしょう。
圧縮が効いた結果、データ容量の84.21%を使っていたものが48.00%まで下がっています。
圧縮データが含まれたテキストファイルをデコードする場合も「-C
」オプションを付けてください。そうしないと「圧縮されていない」と判断し、デコードするため、意図しない結果となります。
データを暗号化して埋め込む
stegsnowはICE暗号化アルゴリズムを利用したデータの暗号化にも対応しています。「-p
」オプションを付けると任意のパスワードを指定可能で、そのパスワードを知っている人だけがデコードできるようになるのです。
まずはパスワード無しでデコードしてみましょう。
うまくデコードできず意味不明の文字列になってしまいましたね。
次はパスワードを付けてデコードします。
今度は無事にデコードできました。
決して強度のある暗号化アルゴリズムではないので、あくまでカジュアルに暗号化できます、程度に考えておきましょう。