hannabaker commited on
Commit
cf65694
·
verified ·
1 Parent(s): 4a07c4f

Create providers/filen_provider.py

Browse files
Files changed (1) hide show
  1. providers/filen_provider.py +257 -0
providers/filen_provider.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import hashlib
5
+ import requests
6
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
7
+ from cryptography.hazmat.primitives import hashes
8
+ from cryptography.hazmat.backends import default_backend
9
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
10
+ import base64
11
+
12
+ class FilenProvider:
13
+ def __init__(self, email=None, password=None):
14
+ self.email = email
15
+ self.password = password
16
+ self.api_key = None
17
+ self.master_keys = []
18
+ self.base_url = "https://gateway.filen.io"
19
+
20
+ def validate_credentials(self, email, password):
21
+ """Validate Filen credentials"""
22
+ try:
23
+ # Get auth info
24
+ auth_info = self._get_auth_info(email)
25
+ if not auth_info:
26
+ return False
27
+
28
+ # Derive password
29
+ derived = self._derive_password(password, auth_info['salt'], auth_info['authVersion'])
30
+
31
+ # Try to login
32
+ login_result = self._login(email, derived['password'], auth_info['authVersion'])
33
+ return login_result is not None
34
+
35
+ except Exception as e:
36
+ print(f"Filen validation error: {e}")
37
+ return False
38
+
39
+ def _get_auth_info(self, email):
40
+ """Get authentication info for email"""
41
+ response = requests.post(
42
+ f"{self.base_url}/v3/auth/info",
43
+ json={"email": email}
44
+ )
45
+
46
+ if response.status_code == 200:
47
+ return response.json()
48
+ return None
49
+
50
+ def _derive_password(self, raw_password, salt, auth_version):
51
+ """Derive password and master key"""
52
+ if auth_version == 2:
53
+ # PBKDF2 derivation
54
+ kdf = PBKDF2HMAC(
55
+ algorithm=hashes.SHA512(),
56
+ length=64,
57
+ salt=salt.encode(),
58
+ iterations=200000,
59
+ backend=default_backend()
60
+ )
61
+
62
+ derived_key = kdf.derive(raw_password.encode())
63
+ derived_hex = derived_key.hex()
64
+
65
+ # Split the key
66
+ master_key = derived_hex[:len(derived_hex)//2]
67
+ password = derived_hex[len(derived_hex)//2:]
68
+
69
+ # Hash password with SHA-512
70
+ password_hash = hashlib.sha512(password.encode()).hexdigest()
71
+
72
+ return {
73
+ 'master_key': master_key,
74
+ 'password': password_hash
75
+ }
76
+ else:
77
+ raise Exception(f"Unsupported auth version: {auth_version}")
78
+
79
+ def _login(self, email, password, auth_version):
80
+ """Login to Filen"""
81
+ response = requests.post(
82
+ f"{self.base_url}/v3/login",
83
+ json={
84
+ "email": email,
85
+ "password": password,
86
+ "authVersion": auth_version,
87
+ "twoFactorCode": "XXXXXX"
88
+ }
89
+ )
90
+
91
+ if response.status_code == 200:
92
+ data = response.json()
93
+ self.api_key = data.get('apiKey')
94
+ return data
95
+ return None
96
+
97
+ def upload(self, file_path, progress_callback=None):
98
+ """Upload file to Filen"""
99
+ try:
100
+ # First, authenticate
101
+ auth_info = self._get_auth_info(self.email)
102
+ derived = self._derive_password(self.password, auth_info['salt'], auth_info['authVersion'])
103
+ login_result = self._login(self.email, derived['password'], auth_info['authVersion'])
104
+
105
+ if not login_result:
106
+ raise Exception("Failed to authenticate with Filen")
107
+
108
+ # Get base folder
109
+ base_folder = self._get_base_folder()
110
+ if not base_folder:
111
+ raise Exception("Failed to get base folder")
112
+
113
+ # Generate file UUID and encryption key
114
+ import uuid
115
+ file_uuid = str(uuid.uuid4())
116
+ file_key = os.urandom(32).hex()[:12]
117
+
118
+ # Read and encrypt file in chunks
119
+ chunk_size = 1024 * 1024 # 1MB chunks
120
+ file_size = os.path.getsize(file_path)
121
+ chunks_uploaded = 0
122
+ total_chunks = (file_size + chunk_size - 1) // chunk_size
123
+
124
+ with open(file_path, 'rb') as f:
125
+ chunk_index = 0
126
+ while True:
127
+ chunk_data = f.read(chunk_size)
128
+ if not chunk_data:
129
+ break
130
+
131
+ # Encrypt chunk
132
+ encrypted_chunk = self._encrypt_chunk(chunk_data, file_key)
133
+
134
+ # Upload chunk
135
+ upload_result = self._upload_chunk(
136
+ file_uuid,
137
+ chunk_index,
138
+ encrypted_chunk,
139
+ base_folder['uuid']
140
+ )
141
+
142
+ if not upload_result:
143
+ raise Exception(f"Failed to upload chunk {chunk_index}")
144
+
145
+ chunk_index += 1
146
+ chunks_uploaded += 1
147
+
148
+ if progress_callback:
149
+ progress = (chunks_uploaded / total_chunks) * 100
150
+ progress_callback(progress)
151
+
152
+ # Mark upload as complete
153
+ self._complete_upload(
154
+ file_uuid,
155
+ os.path.basename(file_path),
156
+ file_key,
157
+ file_size,
158
+ total_chunks,
159
+ base_folder['uuid']
160
+ )
161
+
162
+ return {
163
+ 'provider': 'filen',
164
+ 'filename': os.path.basename(file_path),
165
+ 'size': file_size,
166
+ 'uuid': file_uuid
167
+ }
168
+
169
+ except Exception as e:
170
+ print(f"Filen upload error: {e}")
171
+ return None
172
+
173
+ def _get_base_folder(self):
174
+ """Get user's base folder"""
175
+ response = requests.get(
176
+ f"{self.base_url}/v3/user/baseFolder",
177
+ headers={"Authorization": f"Bearer {self.api_key}"}
178
+ )
179
+
180
+ if response.status_code == 200:
181
+ return response.json()
182
+ return None
183
+
184
+ def _encrypt_chunk(self, data, key):
185
+ """Encrypt a chunk of data"""
186
+ # Generate IV
187
+ iv = os.urandom(12)
188
+
189
+ # Create cipher
190
+ cipher = Cipher(
191
+ algorithms.AES(key.encode()[:32]),
192
+ modes.GCM(iv),
193
+ backend=default_backend()
194
+ )
195
+
196
+ encryptor = cipher.encryptor()
197
+ encrypted = encryptor.update(data) + encryptor.finalize()
198
+
199
+ # Return IV + encrypted data + auth tag
200
+ return iv + encrypted + encryptor.tag
201
+
202
+ def _upload_chunk(self, file_uuid, index, data, parent_uuid):
203
+ """Upload a single chunk"""
204
+ # Calculate hash
205
+ data_hash = hashlib.sha512(data).hexdigest()
206
+
207
+ # Upload
208
+ response = requests.post(
209
+ f"{self.base_url}/v3/upload",
210
+ params={
211
+ "uuid": file_uuid,
212
+ "index": index,
213
+ "parent": parent_uuid,
214
+ "uploadKey": os.urandom(32).hex(),
215
+ "hash": data_hash
216
+ },
217
+ headers={"Authorization": f"Bearer {self.api_key}"},
218
+ data=data
219
+ )
220
+
221
+ return response.status_code == 200
222
+
223
+ def _complete_upload(self, file_uuid, filename, key, size, chunks, parent_uuid):
224
+ """Mark upload as complete"""
225
+ # Encrypt metadata
226
+ metadata = {
227
+ "name": filename,
228
+ "size": size,
229
+ "mime": "application/octet-stream",
230
+ "key": key
231
+ }
232
+
233
+ encrypted_metadata = self._encrypt_metadata(json.dumps(metadata))
234
+
235
+ response = requests.post(
236
+ f"{self.base_url}/v3/upload/done",
237
+ headers={"Authorization": f"Bearer {self.api_key}"},
238
+ json={
239
+ "uuid": file_uuid,
240
+ "name": encrypted_metadata,
241
+ "nameHashed": hashlib.sha512(filename.encode()).hexdigest(),
242
+ "size": size,
243
+ "chunks": chunks,
244
+ "mime": "application/octet-stream",
245
+ "rm": "",
246
+ "metadata": encrypted_metadata,
247
+ "version": 2,
248
+ "uploadKey": ""
249
+ }
250
+ )
251
+
252
+ return response.status_code == 200
253
+
254
+ def _encrypt_metadata(self, metadata):
255
+ """Encrypt metadata string"""
256
+ # Simplified - in production, use proper metadata encryption
257
+ return base64.b64encode(metadata.encode()).decode()