~mtrausch/pymcrypt/trunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
'''mcrypt wrapper for Python 2 and 3
Copyright (C) 2010 Michael B. Trausch <mike@trausch.us>
License: GPL v3 as published by the Free Software Foundation

The EncryptedWriter module is used to create encrypted data streams
from plaintext data streams.  The output data stream contains a header
(which EncryptedReader understands) to identify the decryption
parameters, and a footer to specify how much padding is present (if
padding is necessary).

While the EncryptedWriter class can be used to create files that are
encrypted, it is probably not a terribly good idea to do so.  There is
absolutely no protection against corruption or tampering in this
class. The user can provide such protection for him or herself, if
necessary, or use the HashedEncryptedReader/Writer classes to generate
files where tampering and/or corruption can be detected.'''

import io
import platform
import struct
from .libmcrypt import *
from .common import ALGO_MAP, MODE_MAP

class EncryptedWriter(io.BufferedWriter):
    '''EncryptedWriter(raw, passwd[, algo[, mode]]) -> EncryptedWriter
    A sequential, writable, block-oriented encrypted stream writer

    This is just like io.BufferedWriter, but it encrypts data as it
    writes.  The 'raw' parameter is passed to the io.BufferedWriter
    constructor, and the password parameter is required in order to
    create the key.

    The stream will have a header written to it, which identifies the
    file format to an EncryptedReader object, as well as certain
    parameters required for decryption: the algorithm, the mode, the
    salt, and IV (if applicable).  The PBKDF2 key derivation function
    is the only KDF used for generating the keys for these files.'''
    def __init__(self, raw, password, algo = ALGO_DEFAULT,
                 mode = MODE_DEFAULT):
        pyver = platform.python_version_tuple()

        if pyver[0] == '3':
            self._parentProxy = super()
        else:
            self._parentProxy = super(io.BufferedWriter, self)

        self._parentProxy.__init__(raw)

        self._encrypter = raw_encrypt(algo, mode)
        self._encrypter.set_parameters(password)

        self._encrypter.begin()
        self._write_header()

        self._buffer = bytearray()

        if self._encrypter.get_iv_len() > 0:
            self.write(self._encrypter.get_iv())

        return

    def close(self):
        '''close([pending_behavior]) -> None

        Closes the EncryptedWriter stream.  If there are any bytes
        pending flushing, padding will be appended to them.  Lastly,
        two blocks will be written to the stream: a sentinel block and
        the footer block, which contains the padding length for the
        last block.'''
        if len(self._buffer) == 0:
            self._parentProxy.close()
            return

        to_write = bytes(self._buffer)
        pcount = self._padding(len(to_write))
        to_write += b'\xff' * pcount
        self.write(to_write)

        blksz = self._encrypter.get_block_size()
        self._pwrite(b'\xff' * blksz)
        self._pwrite(struct.pack('!H' + ('x' * (blksz - 2)),
                                 pcount))
        self._parentProxy.close()
        self._encrypter.end()
        return

    def get_block_size(self):
        return self._encrypter.get_block_size()

    def write(self, bytestr):
        '''write(bytestr) -> int
        Write the supplied bytes to the stream.'''
        blksz = self._encrypter.get_block_size()

        self._buffer.extend(bytestr)
        buflen = len(self._buffer)

        (blocks, leftover) = divmod(buflen, blksz)
        if(blocks > 0):
            self._write_blocks(blocks)
        return

    def _padding(self, bytes):
        blksz = self._encrypter.get_block_size()
        pad_len = blksz - (bytes % blksz)
        if pad_len == self._encrypter.get_block_size():
            return 0

        return pad_len

    def _pwrite(self, bytestr):
        return self._parentProxy.write(bytestr)

    def _write_blocks(self, block_count):
        '''_write_blocks(bytes) -> int
        Returns the bytes written'''
        byte_count = block_count * self._encrypter.get_block_size()
        bytesTR = bytes(self._buffer[:byte_count])
        self._buffer = self._buffer[byte_count:]
        encBytes = self._encrypter.step(bytesTR)
        return self._pwrite(encBytes)

    def _write_header(self):
        '''_write_header() -> None
        Writes metainfo header to make decryption possible.'''
        def _pack(length):
            return(struct.pack('!B', length))

        self._pwrite(b'EW0\x00')

        enc = self._encrypter
        params = enc.get_parameters()
        iv = enc.get_iv()
        salt = enc.get_salt()

        _a = params['algorithm'].encode('utf8')
        _m = params['mode'].encode('utf8')
        self._pwrite(_pack(ALGO_MAP[_a]))
        self._pwrite(_pack(MODE_MAP[_m]))

        self._pwrite(_pack(len(salt)))
        self._pwrite(salt)

        if iv is not None:
            self._pwrite(_pack(len(iv)))
            self._pwrite(iv)
        else:
            self._pwrite(b'\x00')

        cpos = self.tell()
        blksz = enc.get_block_size()

        pad_len = blksz - (cpos % blksz)
        if pad_len != blksz:
            self._pwrite(b'\xff' * pad_len)