QRコード動画を使ってzipファイルをやり取りしてみた

:::note alert
悪いことに使える可能性もゼロではないと思いますが、この記事の内容を用いた行動は自己責任でお願いします。
:::

# はじめに
はじめまして。初投稿です。セキュリティコンサルをしております。普段は監査とか法規対応とかその辺をメインで仕事してますので技術屋さんではないです。~~そもそも技術寄りの内容はさせてもらえないですけど~~

さて、世の中には社内の機密情報を社外に持ち出す人間がいます。少しニュースを調べれば顧客情報を持ち出したり、営業秘密を持ち出した事件が出てくると思います。
この手の話になると、「私物のUSBメモリは禁止」とか「個人のメールアドレスにメールを送るのは禁止」とかになって監視するためのソフトありますが、個人的にはずっと
>「HDMIを外部機器に差し込んで録画すればいいんじゃね?」

と思ってました。ExcelとかPDFとかは直接持ち出せなくとも、映像としては外部に持ち出せるんではないか、という話です。監視ソフトがHDMIの端子を監視していたところであまり問題視されない(リモートワークが増えて私物のディスプレイに繋ぐ機会が増えてからはなおさら)ですからね。

では、動画を用いて情報を受け渡しする方法をなんとなく思いついたので、なんとなくテストしてみます。

# 処理の内容
今回行うのは「ファイルをQRコードに変換し、QRコードをつなげた動画を録画することでPC外に持ち出す」という方法です。
このなんとなく思いついた方法、調べ方が悪いのかもっと賢い方法があるのか分からないですが特に出てきませんでしたので、QRコードをつなげた動画をここでは「QRコード動画」と呼びます。
「QRコード動画」は複数のQRコードをただ繋げただけの動画です。なので人間が見ても分かりませんが、現れるQRコードを順次デコードしてくっつければ最終的には元のファイルになります。QRコードを用いているので誤り訂正も出来るし画面録画できなくても、PC画面をスマホで録画できればあとは何とかなるんじゃないですかね。(カメラの記録が30fpsなら動画を15fpsにするとか工夫は必要だと思います)
とりあえずここでは
> 入力: zip
> 出力: mp4

として作成したいと思います。
テストなので画面録画やカメラでの撮影はいったん置いておいてzip→mp4、mp4→zipを試してみます。QRコードはバージョン14で誤り訂正はLとします。(この設定になった理由は後述)
なお、QRコードはバイナリのまま変換できるらしいですが、作るときは知らずにBase64で文字列に変換しています。

# 実装
作成言語はとりあえず動くものが作れるPythonで書きます。
## ソースコード
```python:qrmovie.pyimport argparse
import cv2
import qrcode as qc
import numpy as np
import base64

def zip_to_mp4(zip_path, output_mp4_path):
    QR_VERSION = 14
    ERROR_CORRECTION = qc.constants.ERROR_CORRECT_L
    BOX_SIZE = 10
    BORDER = 4
    FPS = 30

    with open(zip_path, 'rb') as f:
        data = f.read()
    encoded_data = base64.urlsafe_b64encode(data).decode('utf-8')

    max_chars_per_qr = 100  #問題点
    chunk_size = min(max_chars_per_qr, len(encoded_data))
    frames = []
    num_chunks = len(encoded_data) // chunk_size + (1 if len(encoded_data) % chunk_size else 0)

    for i in range(num_chunks):
        chunk = encoded_data[i * chunk_size:(i + 1) * chunk_size]
        qr = qc.QRCode(
            version=QR_VERSION,
            error_correction=ERROR_CORRECTION,
            box_size=BOX_SIZE,
            border=BORDER
        )
        qr.add_data(chunk)
        qr.make()
        img = qr.make_image()
        img_array = np.array(img.convert('RGB'))
        frames.append(img_array)

    if frames:
        height, width, layers = frames[0].shape
        video = cv2.VideoWriter(output_mp4_path, cv2.VideoWriter_fourcc(*'mp4v'), FPS, (width, height))

        for frame in frames:
            video.write(frame)

        video.release()
    else:
        print("No frames")

def mp4_to_zip(mp4_path, output_zip_path):
    video = cv2.VideoCapture(mp4_path)
    decoded_data = ""
    detector = cv2.QRCodeDetector()
    while video.isOpened():
        ret, frame = video.read()
        if not ret:
            break
        chunk, points, _ = detector.detectAndDecode(frame)
        if chunk:
            decoded_data += chunk
    video.release()

    if len(decoded_data) % 4 != 0:
         padding = (4 - len(decoded_data) % 4) % 4
         decoded_data += "=" * padding
    data = base64.urlsafe_b64decode(decoded_data)
    with open(output_zip_path, 'wb') as f:
        f.write(data)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("mode", choices=["ziptomp4", "mp4tozip"])
    parser.add_argument("input_path")
    parser.add_argument("output_path")
    args = parser.parse_args()

    if args.mode == "ziptomp4":
        zip_to_mp4(args.input_path, args.output_path)
    elif args.mode == "mp4tozip":
        mp4_to_zip(args.input_path, args.output_path)

if __name__ == "__main__":
    main()

```

# 実行
手元の適当なファイルを変換したところ、42KBのzipファイルが46MB 18秒のmp4になりました。

# 課題
今回のはテストコードですので課題はたくさんあります。
## ツール自体の課題

### 1. Base64にしているのでデータサイズが大きくなる
Base64にするとざっくりデータサイズは1.4倍になると言いますし、直接バイナリをQRコードにした方がよいと思います。

### 2. 動画サイズが大きい
これは圧縮率とか動画のサイズを小さくすれば多少改善されると思いますが、あとに紹介する4の問題解決が先だと思います。

### 3. 大きいファイルはQRコード動画にできない
今回はとりあえず動かすことが目的だったので気にしなかったのですが、大きいファイルになると変換できません。というのも、QRコード動画を作成する前に、メモリ上にQRコードを連結したデータを一度置いているのでデカいファイルはメモリが足らなくなります。ある程度連結したら一度書き出したりすればいいんじゃないかと思います。

### 4. QRコードの読み込みに失敗することがある
これはどうにも分からず力技です。コード中にはかなり怪しい処理があると思います。具体的には
1. QRコードのバージョンが14
2. QRコード1枚当たりの文字数が100文字という微妙な数字(コメントに書いた問題点)
という部分です。普通に考えたらバージョン40あたりのめちゃめちゃデカいQRコードで上限いっぱいの文字数を設定すればよいと思いますし、当初はそうしていたのですがこれをやると読み込み処理でQRコードを正しく読み込めないことが頻発しました。最初は文字数の設定が悪いのかとかエラー訂正とかそのあたりかと思ったのですが、どうも
1. エラー訂正はLow(低)
2. QRコードのバージョンは14以下
3. QRコード1枚当たりの文字数は余裕を見て適当に決める
としてQRコードに詰められるデータに対して余力を持たせないと、失敗するようです。試した最適解がいまの設定です。検出に失敗しているだけでQRコードにすること自体は問題ないはずなのですが理由は分かりません。

## 手法自体の課題
### 1. ファイルではなく録画した画像から読み出せるか
これが今回の検証で一番大事なのですが久しぶりにコードを少し書いたら疲れてしまいました。単純にQRコード複数枚に分割しているので1つでも読み込めなければ変換できません。画面を丸ごと録画していれば大丈夫だと思いますがカメラ越しとかだと厳しいかもしれません。

### 2. QRコード動画作成に時間がかかる
これも予想の範囲内ですが、QRコード動画を生成するのに時間がかかりますね。これは並列処理でQRコード作成して、最後に順番通りくっつければ多少は速くなりそうです。

### 3. このやり方ができる環境なら別の手段がありそう
そもそも論ですが、このやり方が出来るのは何らかの開発環境が必要なので機密情報を持ち出そうとしたら他に色々やり方がありそうです。こんなめんどくさい方法使うよりも簡単に持ち出せるでしょう。あと、この手法を使って悪いことしても私は責任取れません。

# この手法の現実性と対策
ここまでやって「あんまり現実的じゃねぇな」と思いました。ちょっとしたテキストファイルでも大変なのに数MB持ち出すだけでとんでもない変換時間が必要になりますし動画のサイズもえらいことになります。
現実味はないですが対策は困難かと思います。理由のわからないソフトを作ってたり意味のわからない動画を再生しているところを発見できればよいですが、最終的に外部に持ち出す部分は動画を画面を録画しているだけです。HDMIポートを監視したところでせいぜい抜き差しのログくらいでその先がディスプレイにつながっているのか録画装置につながっているのかはわかりませんし、極端なことを言えば画面をカメラで録画されていてもわからないですからね。

# まとめ
zipファイルをQRコード動画にして元に戻すことは、あたりまえだけどできた。