銀月の符号

Python 使い見習いの日記・雑記

ファイルとファイルの排他的論理和を取って暗号化

http://w3-info.net/post-419.html』を読んで。自分でもやってみたくなったので、やってみた。

Python でファイルとある値、もしくはファイルとファイルの排他的論理和を求めて暗号化する手順について自分なりに考えてみる。

整数列 2 つから排他的論理和

まずは [0, 1, 2] のような整数列、整数を返す iterable 2 つから排他的論理和を求められるようにする。

このための呼び出し可能オブジェクトは functools.patial, itertools.imap, operator.xor を使って作ることが出来る。

import itertools
import functools
import operator

iter_xor = functools.partial(itertools.imap, operator.xor)

もっと単純に、こう書いたほうがわかりやすいかも。まぁ itertools, functools がマイブーム継続中なのでしかたがない(ぉぃ

import itertools

def iter_xor(a, b):
    for c, d in itertools.izip(a, b):
        yield c ^ d

こんな感じで使える。

>>> iter_xor([0, 1, 2, 3], [255, 255, 255, 255])
<itertools.imap object at 0x00AF9E70>
>>> list(_)
[255, 254, 253, 252]

ファイルから整数列を作る

次にファイルから文字列を読み込み、これを整数列に直すことを考える。

ファイルから整数列を作るにはすべて文字列として読み込んだ後、 1 文字ずつ ord にかけていくことも出来るけれど、メモリ上にデータを置かないようにしてみる。遅延評価。ジェネレータの出番。これで巨大ファイルも処理できる。

def file2iterint(f):
    u"""ファイルオブジェクトから整数を得るイテレータを作る"""

    while True:
        b = f.read(1)
        if not b:
            break
        yield ord(b)
>>> from StringIO import StringIO
>>> s = StringIO('abcd')
>>> file2iterint(s)
<generator object file2iterint at 0x00B00080>
>>> list(_)
[97, 98, 99, 100]

効率を気にするなら f.read(8192) のように、ある程度まとめて読み込んで自前のバッファに置くほうがベターかも。とりあえず、効率は置いておく。ベスト解はないし(デバイスの特性とか。あと StringIO.StringIO, urllib.open のようなファイルライクオブジェクトがくる可能性も。)。

これと前述の iter_xor を組み合わせることで、ファイルのビットを反転(255 との排他的論理和)させたり、 2 つのファイルから排他的論理和をとったりできる。

>>> from StringIO import StringIO
>>> s = StringIO('abcd')
>>> i = file2iterint(s)
>>> iter_xor(i, [255, 255, 255])
<itertools.imap object at 0x00B00250>
>>> list(_)
[158, 157, 156]

しかし 2 つの整数列の長さがそろっていない場合、短いほうの長さまでしか処理されない。なので、ファイルの暗号化に用いるには、これでは足りない。この問題は無限長のイテレータを導入することで解決できる。

無限長のイテレータ

Pythonイテレータは無限長のデータを表現することが出来る。たとえば次のようなジェネレータは整数 0 を返し続ける。

def repeat0():
    while True:
        yield 0
>>> r = repeat0()
>>> next(r), next(r), next(r), next(r), next(r), next(r)
(0, 0, 0, 0, 0, 0)

実は itertools.repeat という便利なものがあるので、整数 0 を返し続けるジェネレータはこうも書ける。

>>> import itertools
>>> r = itertools.repeat(0)
>>> next(r), next(r), next(r), next(r), next(r), next(r)
(0, 0, 0, 0, 0, 0)

これらとイテレータイテレータを連結する itertools.chain を組み合わせると、前のイテレータからの供給が止まったらある値を繰り返し返すイテレータを作ることが出来る。

>>> data = [0, 1, 2]
>>> r = itertools.chain(data, itertools.repeat(255))
>>> next(r), next(r), next(r), next(r), next(r), next(r)
(0, 1, 2, 255, 255, 255)

もしくは itertools.cycle を使うと、同じ内容をメモリ上に保持し、繰り返すことが出来る。

>>> r = itertools.cycle(data)
>>> next(r), next(r), next(r), next(r), next(r), next(r)
(0, 1, 2, 0, 1, 2)

今回のファイルの暗号化における鍵となるほうのファイルの処理方法は、この全体を繰り返すアプローチを用いることにする。

ファイルから整数列を作る2・終端に到達したら読み込んだデータを繰り返す

前述の file2iterint と itertools.cycle を使えば、ファイルの内容を整数列に直しつつ、末尾に到達したら繰り返すことはできなくはない。けれども、 file2iterint はメモリ上にデータを置かないように作ったのに itertools.cycle は置く作りになっている。メモリ上にデータを置くかどうかを選べるようにするため、 file2cycleiterint を作ってみる。

メモリ上に置かずにファイルから再読み込みするには、 cycle を使わず、読み込み終了時 seek で 戻れば OK 。

# 再掲
def file2iterint(f):
    u"""ファイルオブジェクトから整数を得るイテレータを作る"""

    while True:
        b = f.read(1)
        if not b:
            break
        yield ord(b)

def file2cycleiterint(f, cache=False):
    u"""ファイルオブジェクトから整数を得て、繰り返し返すイテレータを作る
    
    cache が True の場合、一度読み込んだデータはメモリ上に保持する。
    False の場合、データを保持せずファイルから読み直す。"""

    if cache:
        return itertools.cycle(file2iterint(f))
    else:
        def _():
            pos = f.tell()
            while True:
                for b in file2iterint(f):
                    yield b
                f.seek(pos)
        return _()

使ってみる。

>>> from StringIO import StringIO
>>> s = StringIO('ab')
>>> r = file2cycleiterint(s)
>>> next(r), next(r), next(r), next(r), next(r), next(r)
(97, 98, 97, 98, 97, 98)

整数列をファイルに

最後に整数列を文字列に直してファイルに書き込む方法について。整数は chr で文字列に直せる。これをファイルオブジェクトの write メソッドに渡せばよい。

>>> import itertools
>>> from StringIO import StringIO
>>> f = StringIO()
>>> for c in itertools.imap(chr, [97, 98, 99]):
...   f.write(c)
...
>>> f.getvalue()
'abc'

完成品

必要なコード片がでそろったので、組み合わせてコマンドラインから使えるようにする。 input ファイルを暗号化して output に書き出すスクリプトの出来上がり。

$ xorfile.py -h
Usage: xorfile.py [-k KEYFILE] input output

Options:
  -h, --help            show this help message and exit
  -k KEYFILE, --keyfile=KEYFILE
                        キーファイル

KEYFILE が指定されない時はビットを反転する。 KEYFILE が指定された時は、 input と KEYFILEの内容との排他的論理和をとる。

xorfile.py
# coding: utf-8

u"""ファイルを対象とした排他的論理和
"""

import itertools
import functools
import operator

iter_xor = functools.partial(itertools.imap, operator.xor)
# 以下のコードとほぼ同義
#def iter_xor(a, b):
#    for c, d in itertools.izip(a, b):
#        yield c ^ d
iter_xor.__doc__ = \
    u"""イテレータブル2つから排他的論理和を求めるイテレータを作る"""

def file2iterint(f):
    u"""ファイルオブジェクトから整数を得るイテレータを作る"""

    while True:
        b = f.read(1)
        if not b:
            break
        yield ord(b)

def file2cycleiterint(f, cache=False):
    u"""ファイルオブジェクトから整数を得て、繰り返し返すイテレータを作る
    
    cache が True の場合、一度読み込んだデータはメモリ上に保持する。
    False の場合、データを保持せずファイルから読み直す。"""

    if cache:
        return itertools.cycle(file2iterint(f))
    else:
        def _():
            pos = f.tell()
            while True:
                for b in file2iterint(f):
                    yield b
                f.seek(pos)
        return _()

def main():
    import os
    import optparse

    usage = u'usage: %prog [-k KEYFILE] input output'
    parser = optparse.OptionParser(usage=usage)
    parser.add_option('-k', '--keyfile',
            action='store',
            default=None,
            help=u'キーファイル')

    options, args = parser.parse_args()

    len_args = len(args)
    if len_args < 2:
        parser.error(u'引数が足りません')
    if len_args > 2:
        parser.error(u'引数が多すぎます')

    in_file_name, out_file_name = args[:2]

    if not os.path.exists(in_file_name):
        parser.error(u'ファイル "%s" は存在しません' % in_file_name)
    if options.keyfile and not os.path.exists(options.keyfile):
        parser.error(u'キーファイル "%s" は存在しません' % options.keyfile)

    # in_file_name を開き、整数のイテレータに変換
    in_file = open(in_file_name, 'rb')
    in_ = file2iterint(in_file)

    if options.keyfile:
        # options.keyfile を開き、整数の無限長イテレータに変換。
        # これから得られる値をキーとする
        key_file = open(options.keyfile, 'rb')
        key = file2cycleiterint(key_file)
    else:
        # キーファイルが未指定のときは、 255 をキーとする。
        # その結果、入力ビットはすべて反転される。
        key_file = None
        key = itertools.repeat(255)

    with open(out_file_name, 'wb') as out_file:
        for c in itertools.imap(chr, iter_xor(in_, key)):
            out_file.write(c)

    if key_file:
        key_file.close()
    in_file.close()

if __name__ == '__main__':
    main()