1 import requests
2 from Crypto.PublicKey import RSA
3 from Crypto import Random
4 import os
5 from base64 import b64encode, b64decode, b16encode
6 import json
7 from .cryptfile import encrypt_file, decrypt_file
8
9 rng = Random.new().read
10 DEFAULT_HOST = 'localhost'
11
18 return 'SecurityException: ' + self.msg
19
21 """The base class for interacting with speakeasy"""
24 """username is the username of the speakeasy user
25 pubkey is the user's public key as a string
26 privkey is the user's private key as a string
27 host is the host that speakeasy is running on (default is localhost)
28 password is the password for the private key (default to blank / no password)
29 auth determines whether to connect and authenticate immediately (default to true)"""
30 self.username = username
31 self.password = password
32 self.host = host
33
34 self.pubkey = pubkey
35 self.privkey = privkey
36
37 if auth:
38 self.authenticate()
39
41 """Authenticate to speakeasy. You do not need to call this yourself
42 if you set auth = True in the constructor"""
43 cookiefname = os.path.expanduser('~/.bootlegger/cookiejar.json')
44 write_cookies = True
45
46 if os.path.isfile(cookiefname):
47 f = open(cookiefname)
48 cookies = json.load(f)
49 f.close()
50 if cookies['username'] != self.username:
51 cookies = self._real_auth()
52 else:
53 write_cookies = False
54 else:
55 cookies = self._real_auth()
56
57 self.cookies = dict([(str(key), str(val)) for (key, val) in cookies.items()])
58
59 if write_cookies:
60 f = open(cookiefname, 'w')
61 json.dump(self.cookies, f)
62 f.close()
63
64 - def upload(self, fname, rname = None):
65 """Encrypt and upload the file given by fname to the server.
66 fname should be the full path to the file.
67 You can set the optional argument rname to give the file a
68 different name on the server"""
69 rsakey = RSA.importKey(self.pubkey)
70
71 aes_key = rng(32)
72 tempname = '/tmp/' + b16encode(rng(16)) + '.bootleg'
73 encrypt_file(fname, tempname, aes_key)
74 aes_key = rsakey.encrypt(aes_key, rng(384))[0]
75 aes_key = b64encode(aes_key)
76
77 url = 'http://' + self.host + '/file/upload'
78
79 cryptf = open(tempname)
80
81 if not rname:
82 rname = os.path.basename(fname)
83
84 files = {'file': (rname, cryptf)}
85 headers = {'Symmetric-Key': str(aes_key)}
86
87 r = requests.post(url, cookies=self.cookies, files=files, headers=headers)
88
89 cryptf.close()
90
91 if r.status_code != 200:
92 r.raise_for_status()
93
94 - def download(self, fname, lname = None):
95 """Download and decrypt the file given by fname from the server.
96 By default it creates the file in the current directory with
97 the same name as on the server. To give the downloaded file a
98 different name or download it to a different location, set the
99 lname parameter to where you want the file downloaded."""
100 url = 'http://' + self.host + '/file/download/' + fname
101 tempname = '/tmp/' + b16encode(rng(16)) + '.bootleg'
102 r = requests.get(url, cookies=self.cookies)
103
104 if not lname:
105 lname = fname
106
107 if r.status_code != 200:
108 r.raise_for_status()
109
110 with open(tempname, 'wb') as tempf:
111 for chunk in r.iter_content():
112 tempf.write(chunk)
113
114 aes_key = b64decode(r.headers['Symmetric-Key'])
115 rsakey = RSA.importKey(self.privkey, self.password)
116 aes_key = rsakey.decrypt(aes_key)
117
118 decrypt_file(tempname, lname, aes_key)
119
121 """Get a list of the names of the files stored on the server.
122 If the optional argument pattern is given, the method will
123 only list files matching the pattern.
124 Pattern should by a unix-style file glob."""
125 url = 'http://' + self.host + '/file/list'
126
127 if pattern:
128 url += '/' + pattern
129
130 r = requests.get(url, cookies=self.cookies)
131
132 if r.status_code != 200:
133 r.raise_for_status()
134
135 resp = json.loads(r.text)
136
137 return resp['files']
138
140 """Get more detailed information about the file called fname
141 from the server."""
142 url = 'http://' + self.host + '/file/info/' + fname
143
144 r = requests.get(url, cookies=self.cookies)
145
146 if r.status_code != 200:
147 r.raise_for_status()
148
149 resp = json.loads(r.text)
150
151 return resp['fileinfo']
152
153 - def share(self, fname, recipient):
154 """Share a file called fname stored on the server with the recipient"""
155 finfo = self.get_info(fname)
156
157 rsakey = RSA.importKey(self.privkey, self.password)
158 aes_key = rsakey.decrypt(b64decode(finfo['aes_key']))
159
160 pubkey = self.get_pubkey(recipient)
161 rsakey = RSA.importKey(pubkey)
162
163 aes_key = rsakey.encrypt(aes_key, rng(384))[0]
164 aes_key = b64encode(aes_key)
165
166 headers = {'Symmetric-Key': aes_key}
167 data = {'recipient': recipient, 'filename': fname}
168
169 url = 'http://' + self.host + '/file/share'
170
171 r = requests.post(url, headers=headers, data=data, cookies=self.cookies)
172
173 if r.status_code != 200:
174 r.raise_for_status()
175
177 """Get the dates of previous modifications to the file."""
178 url = 'http://' + self.host + '/file/versions/' + fname
179
180 r = requests.get(url, cookies=self.cookies)
181
182 if r.status_code != 200:
183 r.raise_for_status()
184
185 resp = json.loads(r.text)
186
187 return resp['dates']
188
190 """Delete a file from the server."""
191 url = 'http://' + self.host + '/file/delete/' + fname
192
193 r = requests.post(url, cookies=self.cookies)
194
195 if r.status_code != 200:
196 r.raise_for_status()
197
199 """Get the public key of a user.
200 Takes the user's username as an argument.
201 Returns the public key as a string."""
202 fname = os.path.expanduser('~/.bootlegger/' + username + '_public.pem')
203
204 if os.path.isfile(fname):
205 f = open(fname)
206 s = f.read()
207 f.close()
208 return s
209
210 url = 'http://' + self.host + '/pubkey/' + username
211 r = requests.get(url)
212
213 if r.status_code != 200:
214 r.raise_for_status()
215
216 f = open(fname, 'w')
217 f.write(r.text)
218 f.close()
219
220 return r.text
221
223 url = 'http://' + self.host + '/authenticate'
224 rsakey = RSA.importKey(self.privkey, self.password)
225 shibboleth = 'Mom sent me'
226 signature = rsakey.sign(shibboleth, rng(384))[0]
227 data = {'username': self.username,
228 'shibboleth': shibboleth,
229 'signature': str(signature)}
230
231 r = requests.post(url, data=data)
232
233 if r.status_code != 200:
234 r.raise_for_status()
235
236 if not r.cookies['signature']:
237 raise SecurityException('Server did not return cookie.')
238
239 servkey = self.get_pubkey('server')
240 rsakey = RSA.importKey(servkey)
241 servsig = int(r.cookies['signature'])
242
243 if not rsakey.verify(self.username, (servsig,)):
244 raise SecurityException('Could not verify server signature')
245
246 return r.cookies
247
249 """Add a public key to the server."""
250 rsakey = RSA.importKey(self.privkey, self.password)
251 shibboleth = 'Rosie sent me'
252 signature = rsakey.sign(shibboleth, rng(384))[0]
253 data = {'username': self.username,
254 'shibboleth': shibboleth,
255 'signature': str(signature),
256 'pubkey': self.pubkey}
257
258 url = 'http://' + self.host + '/pubkey/add'
259
260 r = requests.post(url, data=data)
261
262 if r.status_code != 200:
263 r.raise_for_status()
264
265 if not r.cookies['signature']:
266 raise SecurityException('Server did not return cookie')
267
268 servkey = self.get_pubkey('server')
269 rsakey = RSA.importKey(servkey)
270 servsig = int(r.cookies['signature'])
271
272 if not rsakey.verify(self.username, (servsig,)):
273 raise SecurityException('Could not verify server signature')
274
275 return r.cookies
276