SHA256
feat: encrypted records storage model + SQLite layer (ciphertext-only)
Adds the base project setup (requirements, README, gitignore entries) and a SQLite storage layer that persists only encrypted records: uuid, ciphertext, nonce, salt, plus a monotonic version counter used later for tamper/replay detection. Includes a smoke test run. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
@@ -174,3 +174,10 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
|
||||||
|
# project
|
||||||
|
venv/
|
||||||
|
instance/
|
||||||
|
certs/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
# crushdb
|
# crushdb – verschlüsselte Datensätze zwischen Personen
|
||||||
|
|
||||||
a secure database for saving transaktions betwen persons
|
Sichere Datenbank für Transaktionen zwischen zwei Personen
|
||||||
|
(Person A, Person B, Timestamp-Start, Timestamp-Ende, Aktion).
|
||||||
|
|
||||||
|
Alle Datensätze werden **im Browser** verschlüsselt
|
||||||
|
(XChaCha20-Poly1305, Schlüsselableitung per Argon2id aus der Passphrase).
|
||||||
|
Der Server sieht ausschließlich Chiffretext.
|
||||||
|
|
||||||
|
## Starten (HTTPS, Dev mit self-signed Zertifikat)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
./venv/bin/pip install -r requirements.txt
|
||||||
|
./run.sh
|
||||||
|
# → https://127.0.0.1:8443
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein selbstsigniertes Zertifikat wird bei Bedarf automatisch erzeugt
|
||||||
|
(`certs/`). Im Browser auftretende Zertifikatswarnungen in der
|
||||||
|
Entwicklung bestätigen.
|
||||||
|
|
||||||
|
Siehe `spezification.txt` für die Anforderungen.
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""SQLite storage layer: stores ONLY ciphertext records.
|
||||||
|
|
||||||
|
Record layout (all client-provided fields, opaque to the server):
|
||||||
|
id INTEGER PRIMARY KEY server surrogate key
|
||||||
|
uuid TEXT UNIQUE NOT NULL client-generated record id
|
||||||
|
ciphertext BLOB NOT NULL XChaCha20-Poly1305 payload (base64 in API)
|
||||||
|
nonce BLOB NOT NULL random 24-byte XChaCha nonce (base64 in API)
|
||||||
|
salt BLOB NOT NULL per-record Argon2id salt (base64 in API)
|
||||||
|
version INTEGER NOT NULL monotonic counter, bump on every update
|
||||||
|
created_at TEXT NOT NULL first seen server time (UTC ISO)
|
||||||
|
updated_at TEXT NOT NULL last update server time (UTC ISO)
|
||||||
|
|
||||||
|
The server never sees plaintext: no field contains decrypted data and
|
||||||
|
nothing is logged except the UUID/version bookkeeping.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
DB_PATH = "instance/crushdb.sqlite3"
|
||||||
|
|
||||||
|
|
||||||
|
def _connect():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute("PRAGMA foreign_keys = ON")
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
"""Create tables if they do not exist yet."""
|
||||||
|
import os
|
||||||
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
|
with _connect() as conn:
|
||||||
|
conn.executescript(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
uuid TEXT NOT NULL UNIQUE,
|
||||||
|
ciphertext BLOB NOT NULL,
|
||||||
|
nonce BLOB NOT NULL,
|
||||||
|
salt BLOB NOT NULL,
|
||||||
|
version INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_uuid ON records(uuid);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _now():
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def _row_to_dict(row):
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"uuid": row["uuid"],
|
||||||
|
"ciphertext": row["ciphertext"],
|
||||||
|
"nonce": row["nonce"],
|
||||||
|
"salt": row["salt"],
|
||||||
|
"version": row["version"],
|
||||||
|
"created_at": row["created_at"],
|
||||||
|
"updated_at": row["updated_at"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_records():
|
||||||
|
with _connect() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM records ORDER BY id ASC"
|
||||||
|
).fetchall()
|
||||||
|
return [_row_to_dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_record(uuid):
|
||||||
|
with _connect() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM records WHERE uuid = ?", (uuid,)
|
||||||
|
).fetchone()
|
||||||
|
return _row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def create_record(uuid, ciphertext, nonce, salt):
|
||||||
|
"""Insert a new encrypted record. Returns the stored dict with server metadata."""
|
||||||
|
now = _now()
|
||||||
|
with _connect() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO records (uuid, ciphertext, nonce, salt, version, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, 1, ?, ?)
|
||||||
|
""",
|
||||||
|
(uuid, ciphertext, nonce, salt, now, now),
|
||||||
|
)
|
||||||
|
row_id = cur.lastrowid
|
||||||
|
row = conn.execute("SELECT * FROM records WHERE id = ?", (row_id,)).fetchone()
|
||||||
|
return _row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def update_record(uuid, ciphertext, nonce, salt, expected_version):
|
||||||
|
"""Replace ciphertext only if the client's version matches the stored one.
|
||||||
|
|
||||||
|
Returns the updated record dict, or None if:
|
||||||
|
- no record with that uuid exists
|
||||||
|
- expected_version does not match (someone else updated / replay attack)
|
||||||
|
"""
|
||||||
|
with _connect() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT version FROM records WHERE uuid = ?", (uuid,)
|
||||||
|
).fetchone()
|
||||||
|
if row is None or row["version"] != expected_version:
|
||||||
|
return None
|
||||||
|
cur = conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE records
|
||||||
|
SET ciphertext = ?, nonce = ?, salt = ?, version = version + 1, updated_at = ?
|
||||||
|
WHERE uuid = ? AND version = ?
|
||||||
|
""",
|
||||||
|
(ciphertext, nonce, salt, _now(), uuid, expected_version),
|
||||||
|
)
|
||||||
|
if cur.rowcount == 0:
|
||||||
|
return None
|
||||||
|
row = conn.execute("SELECT * FROM records WHERE uuid = ?", (uuid,)).fetchone()
|
||||||
|
return _row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_record(uuid, expected_version):
|
||||||
|
"""Delete a record only if the client's version matches. Returns True on success."""
|
||||||
|
with _connect() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"DELETE FROM records WHERE uuid = ? AND version = ?",
|
||||||
|
(uuid, expected_version),
|
||||||
|
)
|
||||||
|
return cur.rowcount > 0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
flask>=3.0
|
||||||
Reference in New Issue
Block a user