Initial commit

This commit is contained in:
2025-09-30 16:18:26 +00:00
commit f9fbd2d98a
9 changed files with 961 additions and 0 deletions

52
client/api.py Normal file
View File

@@ -0,0 +1,52 @@
import requests
class APIError(Exception):
"A custom error we'll use when something goes wrong with the API"
class SubRosaAPI:
"""An API for the SubRosa server.
"""
def __init__(self, url):
self.url = url
def get_user(self, username):
route = "/users"
params = {'name': username}
return self.get(route, params)
def create_user(self, username, public_key):
route = "/users/new"
params = {'name': username, 'public_key': public_key}
return self.post(route, params)
def get_messages(self, username):
route = "/messages"
params = {'name': username}
return self.get(route, params)
def send_message(self, sender, recipient, ciphertext, time_sent, time_sent_signature):
route = "/messages/send"
params = {
'sender': sender,
'recipient': recipient,
'ciphertext': ciphertext,
'time_sent': time_sent,
'time_sent_signature': time_sent_signature,
}
return self.get(route, params)
def get(self, route, params):
response = requests.get(self.url + route, json=params)
if response.ok:
return response.json()
else:
raise APIError(response.json()['error'])
def post(self, route, params):
response = requests.post(self.url + route, json=params)
if response.ok:
return response.json()
else:
raise APIError(response.json()['error'])

106
client/user_interface.py Normal file
View File

@@ -0,0 +1,106 @@
from pathlib import Path
from api import SubRosaAPI, APIError
from datetime import datetime
import sys
sys.path.insert(0, ".")
from encryption import PrivateKey, PublicKey
class SubRosaUI:
"""A user interface to the SubRosa chat system.
"""
def __init__(self, private_key_file, url):
self.api = SubRosaAPI(url)
if Path(private_key_file).exists():
self.private_key = PrivateKey.load(private_key_file)
else:
self.private_key = PrivateKey.generate()
self.private_key.save(private_key_file)
self.public_key = self.private_key.get_public_key()
def run(self, username):
"""Runs the client UI.
"""
self.username = username
self.user = self.get_or_create_user(username)
print("Welcome to SubRosa.")
while True:
choice = self.choose_action_from_menu()
if choice == 'VIEW':
self.show_messages()
elif choice == 'SEND':
self.send_message()
print("=" * 80)
def get_or_create_user(self, username):
"""Gets user info from the server, or creates the user.
If the user already exists, checks that our public key matches
the server's public key for the user.
"""
try:
user = self.api.get_user(username)
if user['public_key'] != str(self.public_key):
raise ValueError(f"Invalid key for {username}")
except APIError:
user = self.api.create_user(username, str(self.public_key))
return user
def choose_action_from_menu(self):
"""Gets a choice from the user.
"""
print("What would you like to do?")
print("1. See messages")
print("2. Send a message")
while True:
choice = input("> ").strip()
if choice == '1':
return 'VIEW'
elif choice == '2':
return 'SEND'
def show_messages(self):
"""Fetches all messages and displays them.
Note: The messages arrive from the server encrypted.
"""
messages = self.api.get_messages(self.username)['messages']
if messages:
for i, message in enumerate(messages):
print('-' * 80)
print(f"{i}. From {message['sender']}:")
print(message['ciphertext'])
else:
print(f"No messages for {self.username}.")
def send_message(self):
"""Sends a message.
"""
try:
recipient_name = input("Username of recipient: ").strip()
recipient = self.api.get_user(recipient_name)
except APIError:
print(f"No user named {recipient_name}.")
return
plaintext = input("Message: ")
print('-' * 80)
print(f"Sorry, couldn't send a message to {recipient_name}; this method isn't implemented yet.")
def get_current_time(self):
return datetime.utcnow().isoformat()
def main():
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("username", help="Existing or new username.")
parser.add_argument("-k", "--key",
default="subrosa_private_key.pem",
help="Private key file. Will be created if it does not exist."
)
parser.add_argument("-s", "--server-url",
default="https://subrosa.makingwithcode.org",
help='Server URL. Use "http://127.0.0.1:8000" for a local server.'
)
args = parser.parse_args()
ui = SubRosaUI(args.key, args.server_url)
ui.run(args.username)
if __name__ == '__main__':
main()