feat: Cryptgeon client with AES-GCM client-side encryption
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -257,6 +257,58 @@ def build_ovpn(
|
||||
return str(out_file.resolve())
|
||||
|
||||
|
||||
# ============================================================
|
||||
# === CRYPTGEON ===
|
||||
# ============================================================
|
||||
|
||||
|
||||
class CryptgeonError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def create_note(content: str, base_url: str) -> str:
|
||||
"""AES-256-GCM encrypt content, POST to Cryptgeon, return one-time URL.
|
||||
|
||||
Encryption matches the Cryptgeon browser client:
|
||||
raw_key (32 bytes) → URL fragment (base64url, no padding)
|
||||
aes_key = SHA-256(raw_key) → AES-256-GCM
|
||||
payload = list(nonce + ciphertext)
|
||||
"""
|
||||
if AESGCM is None:
|
||||
raise CryptgeonError("cryptography package not installed (pip install cryptography)")
|
||||
|
||||
raw_key = os.urandom(32)
|
||||
aes_key = hashlib.sha256(raw_key).digest()
|
||||
nonce = os.urandom(12)
|
||||
aesgcm = AESGCM(aes_key)
|
||||
ciphertext = aesgcm.encrypt(nonce, content.encode("utf-8"), None)
|
||||
|
||||
payload = json.dumps({
|
||||
"contents": list(nonce + ciphertext),
|
||||
"views": 1,
|
||||
"type": "text",
|
||||
}).encode("utf-8")
|
||||
|
||||
api_url = base_url.rstrip("/") + "/api/notes/"
|
||||
req = urllib.request.Request(
|
||||
api_url,
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
body = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise CryptgeonError(f"Cryptgeon API returned {exc.code}: {exc.reason}") from exc
|
||||
except Exception as exc:
|
||||
raise CryptgeonError(f"Cryptgeon request failed: {exc}") from exc
|
||||
|
||||
note_id = body["id"]
|
||||
key_fragment = base64.urlsafe_b64encode(raw_key).rstrip(b"=").decode()
|
||||
return f"{base_url.rstrip('/')}/#/note/{note_id}/{key_fragment}"
|
||||
|
||||
|
||||
# (remaining sections added in later tasks)
|
||||
# ============================================================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user