Skip to content

Commit f30aae8

Browse files
authored
chore: drop /data endpoint and server-side decryption (#64)
1 parent 711051f commit f30aae8

5 files changed

Lines changed: 36 additions & 95 deletions

File tree

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,33 @@ Available steps:
160160

161161
### Fetch a package data
162162

163-
* `GET /process/<package_id>/data`
163+
* `GET /process/<package_id>/blob`
164164

165-
Response: the Discord Data Package SQLite database (GZIP of the binary SQLite file), decrypted.
165+
Returns a short-lived presigned S3 URL the client downloads the encrypted
166+
SQLite from directly. Decryption happens client-side using the UPN as the
167+
key, so the encryption key never reaches the server.
168+
169+
Response:
170+
```json
171+
{
172+
"url": "https://<bucket>.s3.<region>.amazonaws.com/...",
173+
"iv": "abc123...",
174+
"ttl": 300
175+
}
176+
```
177+
178+
`iv` is a hex string for real packages, `null` for the demo (the demo blob
179+
is unencrypted). Decrypt with `AES-CBC` using `SHA-256(UPN)` as the key
180+
and the returned IV; the plaintext is a gzipped SQLite file.
166181

167182
Status codes:
168-
* `200`: the data is available and has been returned.
183+
* `200`: presigned URL returned.
169184
* `401`: the UPN KEY provided in the Authorization header is not valid.
170-
* `404`: unknown package ID.
185+
* `404`: no blob for this package id.
186+
* `501`: server doesn't have the S3 backend wired up.
187+
188+
Demo (`/process/demo/blob`) is unauthenticated and lazy-seeds the blob on
189+
the first call after deploy.
171190

172191
### Fetch a package user
173192

src/app.py

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import socket
22

3-
from flask import Flask, jsonify, request, Response, make_response
3+
from flask import Flask, jsonify, request, make_response
44
from flask_cors import CORS
55

66
from flask_limiter import Limiter
77

88
# Import tasks before db so dotenv loads before SQLAlchemy reads POSTGRES_URL.
99
import tasks # noqa: F401
1010
from enqueue import enqueue_package
11-
from db import PackageProcessStatus, SavedPackageData, Session, fetch_package_status, fetch_package_data, fetch_package_rank
11+
from db import PackageProcessStatus, SavedPackageData, Session, fetch_package_status, fetch_package_rank
1212

1313
from sqlite import generate_demo_database
1414

@@ -195,44 +195,6 @@ def get_package_status(package_id):
195195
return jsonify(res), 200
196196

197197

198-
@app.route('/process/<package_id>/data', methods=['GET'])
199-
def get_package_data(package_id):
200-
201-
if package_id == 'demo':
202-
session = Session()
203-
data = fetch_package_data('demo', 'demo', session)
204-
if not data:
205-
binary_data = generate_demo_database()
206-
session.add(SavedPackageData(package_id='demo', encrypted_data=binary_data, iv='demo'))
207-
session.commit()
208-
data = binary_data
209-
session.close()
210-
return Response(data, mimetype='application/octet-stream')
211-
212-
(is_auth, auth_upn) = check_authorization_bearer(request, package_id)
213-
if not is_auth:
214-
return make_response('', 401)
215-
216-
session = Session()
217-
data = fetch_package_data(package_id, auth_upn, session)
218-
session.close()
219-
220-
if not data:
221-
return make_response('', 404)
222-
223-
send_internal_notification({
224-
'embeds': [
225-
{
226-
'title': 'Fetching existing package data',
227-
'description': f'Package ID: {package_id}',
228-
'color': 0xaea4e0
229-
}
230-
]
231-
})
232-
233-
return Response(data, mimetype='application/octet-stream')
234-
235-
236198
@app.route('/process/<package_id>/blob', methods=['GET'])
237199
def get_package_blob(package_id):
238200
"""Return a presigned S3 URL the client can fetch the package blob from.
@@ -367,11 +329,3 @@ def page_not_found(e):
367329
@app.errorhandler(500)
368330
def internal_server_error(e):
369331
return jsonify({'error': 'Internal server error.'}), 500
370-
371-
def rm_demo():
372-
session = Session()
373-
session.query(SavedPackageData).filter(SavedPackageData.package_id == 'demo').delete()
374-
session.commit()
375-
session.close()
376-
377-
rm_demo()

src/crypto.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,3 @@ def encrypt_sqlite_data(sqlite_buffer, auth_upn):
2727

2828
return (encrypted_data, iv)
2929

30-
def decrypt_sqlite_data(encrypted_data, iv, auth_upn):
31-
32-
fixed_size_upn = hashlib.sha256(auth_upn.encode()).digest()
33-
34-
if not isinstance(iv, bytes):
35-
iv = bytes.fromhex(iv[2:])
36-
37-
# Prepare the decryptor
38-
cipher = Cipher(algorithms.AES(fixed_size_upn), modes.CBC(iv), backend=default_backend())
39-
decryptor = cipher.decryptor()
40-
41-
# Decrypt the data
42-
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
43-
44-
# Remove the padding
45-
unpadder = padding.PKCS7(128).unpadder()
46-
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
47-
48-
return unpadded_data

src/db.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
from crypto import decrypt_sqlite_data
21
import os
32

43
from datetime import datetime
5-
from sqlalchemy import create_engine, Column, Integer, String, DateTime, LargeBinary, Boolean
4+
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Boolean
65
from sqlalchemy.orm import declarative_base, sessionmaker
76
from sqlalchemy.sql import func, text
87

@@ -15,7 +14,6 @@ class SavedPackageData(Base):
1514

1615
id = Column(Integer, primary_key=True)
1716
package_id = Column(String(255), nullable=False)
18-
encrypted_data = Column(LargeBinary(), nullable=False)
1917
iv = Column(String(255), nullable=False)
2018
created_at = Column(DateTime, nullable=False, default=func.now())
2119
updated_at = Column(DateTime, nullable=False, onupdate=func.now(), default=func.now())
@@ -41,6 +39,12 @@ class PackageProcessStatus(Base):
4139

4240
Base.metadata.create_all(engine)
4341

42+
# One-shot migration: drop the legacy encrypted_data blob column on the
43+
# saved_package_data table. Encrypted bytes live in S3 now; only iv stays
44+
# in the DB. IF EXISTS makes this idempotent across cold starts.
45+
with engine.begin() as conn:
46+
conn.execute(text("ALTER TABLE saved_package_data DROP COLUMN IF EXISTS encrypted_data;"))
47+
4448
Session = sessionmaker(bind=engine)
4549

4650
def update_progress (package_status_id, package_id, current_progress, session):
@@ -89,16 +93,6 @@ def fetch_package_status(package_id, session):
8993
status = session.query(PackageProcessStatus).filter_by(package_id=package_id).order_by(PackageProcessStatus.created_at.desc()).first()
9094
return status
9195

92-
def fetch_package_data(package_id, auth_upn, session):
93-
result = session.query(SavedPackageData).filter_by(package_id=package_id).order_by(SavedPackageData.created_at.desc()).first()
94-
if result:
95-
if package_id == 'demo':
96-
return result.encrypted_data
97-
encrypted_data = result.encrypted_data
98-
iv = result.iv
99-
sqlite_buffer = decrypt_sqlite_data(encrypted_data, iv, auth_upn)
100-
return sqlite_buffer
101-
10296
def fetch_pending_packages():
10397
session = Session()
10498
# select * from package_process_status pps where step <> 'PROCESSED' and error_message_code is null

src/tasks.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -875,18 +875,11 @@ def read_analytics_file(package_status_id, package_id, link, session):
875875
key = extract_key_from_discord_link(link)
876876
(data, iv) = encrypt_sqlite_data(zipped_buffer, key)
877877

878-
# When PACKAGE_DATA_BUCKET is configured, also push the encrypted blob to
879-
# S3 so /blob can serve a presigned URL. Failure here is non-fatal — the
880-
# legacy DB blob still gets written below and the slow /data path keeps
881-
# working.
878+
# Encrypted blob → S3 (clients fetch via presigned URL); only the iv
879+
# stays in Postgres so the API can hand it to the client for decryption.
882880
import blob_storage
883-
if blob_storage.is_enabled():
884-
try:
885-
blob_storage.upload(package_id, data)
886-
except Exception as e:
887-
print(f'WARN: S3 blob upload failed for {package_id}: {e}')
888-
889-
session.add(SavedPackageData(package_id=package_id, encrypted_data=data, iv=iv))
881+
blob_storage.upload(package_id, data)
882+
session.add(SavedPackageData(package_id=package_id, iv=iv))
890883
session.commit()
891884

892885
print(f'SQLite serialization: {time.time() - start}')

0 commit comments

Comments
 (0)