github / src / safe.py @ 0ce7e35f
1 |
# Copyright (c) 2016, LE GOFF Vincent |
---|---|
2 |
# All rights reserved.
|
3 |
|
4 |
# Redistribution and use in source and binary forms, with or without
|
5 |
# modification, are permitted provided that the following conditions are met:
|
6 |
|
7 |
# * Redistributions of source code must retain the above copyright notice, this
|
8 |
# list of conditions and the following disclaimer.
|
9 |
|
10 |
# * Redistributions in binary form must reproduce the above copyright notice,
|
11 |
# this list of conditions and the following disclaimer in the documentation
|
12 |
# and/or other materials provided with the distribution.
|
13 |
|
14 |
# * Neither the name of ytranslate nor the names of its
|
15 |
# contributors may be used to endorse or promote products derived from
|
16 |
# this software without specific prior written permission.
|
17 |
|
18 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19 |
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
20 |
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
21 |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
22 |
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
23 |
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
24 |
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
25 |
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
26 |
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
27 |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28 |
|
29 |
|
30 |
"""This file contains the 'safe' system of CocoMUD, ways to crypt/encrypt.
|
31 |
|
32 |
This feature requires:
|
33 |
pbkdf2
|
34 |
pyaes
|
35 |
|
36 |
The module contains a class named 'Safe', that should be insantiated
|
37 |
in order to manipulate the encrypting
|
38 |
/decrypting mechanism. This class requires a passphrase in
|
39 |
argument. You can insantiate it as follows:
|
40 |
>>> from safe import Safe
|
41 |
>>> safe = Safe(file=".passphrase")
|
42 |
>>> # (If the file doesn't exist, it will be created with an auto-generated
|
43 |
>>> # passphrase.)
|
44 |
>>> # Alternatively you can specify the passphrase directly
|
45 |
>>> safe = Safe(passphrase="Dsm18fvdjP9sz801,9DJA.1356gndYJz987v")
|
46 |
>>> # Store encrypted data
|
47 |
>>> safe.store("login", "kredh")
|
48 |
>>> safe.store("password", "YoudWishIToldYou")
|
49 |
>>> # Retrieve the data (can be later)
|
50 |
>>> login = safe.retrieve("login")
|
51 |
>>> password = safe.retrieve("password")
|
52 |
|
53 |
Note that datas that is not a string (like a bool or float) will be
|
54 |
saved as unprotected data. If you want to save it encrypted, you can
|
55 |
convert it to string.
|
56 |
|
57 |
"""
|
58 |
|
59 |
import base64 |
60 |
import os |
61 |
import pickle |
62 |
|
63 |
import pyaes |
64 |
from pbkdf2 import PBKDF2 |
65 |
|
66 |
class Safe: |
67 |
|
68 |
"""A safe object, to encrypt/decrypt information.
|
69 |
|
70 |
The Safe class requires a passphrase to be created. This is a
|
71 |
string of characters that adds to the security of encryption.
|
72 |
Obviously, it needs to remain similar to decrypt information that
|
73 |
has been encrypted. Other optional parameters are also possible:
|
74 |
secret: the path of the file in which to store crypted data.
|
75 |
|
76 |
|
77 |
"""
|
78 |
|
79 |
def __init__(self, passphrase=None, file=None, secret="data.crypt", |
80 |
load=True):
|
81 |
self.salt_seed = 'mkhgts465wef4fwtdd' |
82 |
self.passphrase = passphrase
|
83 |
self.secret = secret
|
84 |
self.passphrase_size = 64 |
85 |
self.key_size = 32 |
86 |
self.block_size = 16 |
87 |
self.iv_size = 16 |
88 |
self.salt_size = 8 |
89 |
self.data = {}
|
90 |
|
91 |
if file and os.path.exists(file): |
92 |
with open(file, "r") as pass_file: |
93 |
self.passphrase = pass_file.read()
|
94 |
|
95 |
if not self.passphrase: |
96 |
self.passphrase = base64.b64encode(os.urandom(
|
97 |
self.passphrase_size))
|
98 |
if file: |
99 |
with open(file, "wb") as pass_file: |
100 |
pass_file.write(self.passphrase)
|
101 |
|
102 |
# Load the secret file
|
103 |
if load:
|
104 |
self.load()
|
105 |
|
106 |
def get_salt_from_key(self, key): |
107 |
return PBKDF2(key, self.salt_seed).read(self.salt_size) |
108 |
|
109 |
def encrypt(self, plaintext, salt): |
110 |
"""Pad plaintext, then encrypt it.
|
111 |
|
112 |
The encryption occurs with a new, randomly initialised cipher.
|
113 |
This method will not preserve trailing whitespace in plaintext!.
|
114 |
|
115 |
"""
|
116 |
# Initialise Cipher Randomly
|
117 |
init_vector = os.urandom(self.iv_size)
|
118 |
|
119 |
# Prepare cipher key
|
120 |
key = PBKDF2(self.passphrase, salt).read(self.key_size) |
121 |
cipher = pyaes.AESModeOfOperationCBC(key, iv=init_vector) |
122 |
|
123 |
bs = self.block_size
|
124 |
if isinstance(plaintext, str): |
125 |
plaintext = plaintext.encode("utf-8")
|
126 |
|
127 |
return init_vector + cipher.encrypt(plaintext + \
|
128 |
b" " * (bs - (len(plaintext) % bs))) |
129 |
|
130 |
def decrypt(self, ciphertext, salt): |
131 |
"""Reconstruct the cipher object and decrypt.
|
132 |
|
133 |
This method will not preserve trailing whitespace in the
|
134 |
retrieved value.
|
135 |
|
136 |
"""
|
137 |
# Prepare cipher key
|
138 |
key = PBKDF2(self.passphrase, salt).read(self.key_size) |
139 |
|
140 |
# Extract IV
|
141 |
init_vector = ciphertext[:self.iv_size]
|
142 |
ciphertext = ciphertext[self.iv_size:]
|
143 |
|
144 |
cipher = pyaes.AESModeOfOperationCBC(key, iv=init_vector) |
145 |
|
146 |
decrypted = cipher.decrypt(ciphertext).rstrip(b" ")
|
147 |
return decrypted.decode("utf-8") |
148 |
|
149 |
def load(self): |
150 |
"""Load the data from the 'secret' file if exists."""
|
151 |
if os.path.exists(self.secret): |
152 |
with open(self.secret, "rb") as file: |
153 |
upic = pickle.Unpickler(file, encoding="utf-8") |
154 |
self.data = upic.load()
|
155 |
|
156 |
if not isinstance(self.data, dict): |
157 |
raise ValueError("the data contained in the file " \ |
158 |
"'{}' is not a dictionary".format(self.secret)) |
159 |
|
160 |
def retrieve(self, key, *default): |
161 |
"""Retrieve and decrypt the specified key.
|
162 |
|
163 |
If the key isn't present in the dictionary, either
|
164 |
return default if specified, or raise a KeyError.
|
165 |
|
166 |
If the value at this location isn't a string, return it as is.
|
167 |
|
168 |
"""
|
169 |
if key not in self.data: |
170 |
if default:
|
171 |
return default[0] |
172 |
|
173 |
raise KeyError(key) |
174 |
|
175 |
value = self.data[key]
|
176 |
if isinstance(value, (bytes, str)): |
177 |
salt = self.get_salt_from_key(key)
|
178 |
return self.decrypt(value, salt) |
179 |
|
180 |
return value
|
181 |
|
182 |
def store(self, key, value): |
183 |
"""Store the key in the file.
|
184 |
|
185 |
If the key already exists, replaces it.
|
186 |
If the value is not a string, it will be stored
|
187 |
WITHOUT encryption.
|
188 |
|
189 |
"""
|
190 |
if isinstance(value, str): |
191 |
salt = self.get_salt_from_key(key)
|
192 |
crypted = self.encrypt(value, salt)
|
193 |
self.data[key] = crypted
|
194 |
else:
|
195 |
self.data[key] = value
|
196 |
|
197 |
# Write the new data in the file
|
198 |
self.save()
|
199 |
|
200 |
def save(self): |
201 |
"""Save the data in the secret file."""
|
202 |
with open(self.secret, "wb") as file: |
203 |
pic = pickle.Pickler(file)
|
204 |
pic.dump(self.data)
|