diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..e33ad77 --- /dev/null +++ b/cli.py @@ -0,0 +1,293 @@ +import os +import sys +import time +import requests +from typing import Optional +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization + + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "wikideck"))); + +from Mine.Block import Block +from Mine.Transaction import Transaction + +WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/'); + + +rsa_private_key: Optional[rsa.RSAPrivateKey] = None; +msg_array: list[str] = [""]; +private_key_file: str = "privatekey.pem"; +public_key_file: str = "publickey.pem"; + + +def main(): + + while True: + + if (msg_array[0]): + print(f"Last Action: {msg_array[0]}"); + + print("\n--- Wikideck CLI ---"); + print("1. Create RSA Keypair"); + print("2. Save Private Key"); + print("3. Load RSA Keypair"); + print("4. Save Public Key") + print("5. Mine a Block"); + print("6. Generate a Transaction"); + print("7. View Deck"); + print("0. Exit"); + print("\n-------------------"); + + try: + choice: int = int(input("Select an option: ").strip()); + except ValueError: + msg_array[0] = "Invalid Input. Please Choose a Number Between 0-6."; + clear_console(); + continue; + + + if choice == 0: + break; + elif choice == 1: + opt_generate_keypair(65537, 2048); + elif choice == 2: + opt_save_private_key(rsa_private_key); + elif choice == 3: + opt_load_private_key(); + elif choice == 4: + opt_save_public_key(rsa_private_key); + elif choice == 5: + opt_mine_block(); + elif choice == 6: + opt_generate_transaction(); + elif choice == 7: + opt_view_deck(); + else: + msg_array[0] = "Invalid Input. Please Choose a Number Between 0-6."; + + clear_console(); + +def opt_generate_keypair(exponent=65537, size=2048): + global rsa_private_key; + rsa_private_key = rsa.generate_private_key( + public_exponent = exponent, + key_size = size, + ) + msg_array[0] = "RSA Keypair Generated"; + return rsa_private_key; + +def opt_save_private_key(rsa, filename=private_key_file): + if rsa is None: + msg_array[0] = "No RSA Keypair to extract private key. Please generate one first."; + return None; + else: + with open(filename, "wb") as file: + pem = rsa.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + file.write(pem); + msg_array[0] = f"RSA Private Key Saved to {filename}"; + +def opt_save_public_key(rsa, filename=public_key_file): + if rsa is None: + msg_array[0] = "No RSA Keypair to extract public key. Please generate one first."; + return None; + + public_key = rsa.public_key(); + + public_pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + with open(filename, "wb") as file: + file.write(public_pem); + msg_array[0] = f"RSA Public Key Saved to {filename}"; + +def opt_load_private_key(): + global rsa_private_key; + if not os.path.exists(private_key_file): + msg_array[0] = f"No saved key found ({private_key_file} missing)"; + return None; + + with open(private_key_file, "rb") as file: + rsa_private_key = serialization.load_pem_private_key( + file.read(), + password=None, + ) + msg_array[0] = f"RSA Private Key Loaded From {private_key_file}"; + +def get_private_key_from_file(path): + if not os.path.exists(path): + msg_array[0] = f"No key file found ({path} missing)"; + return None; + + with open(path, "rb") as file: + key = serialization.load_pem_private_key( + file.read(), + password=None, + ); + msg_array[0] = f"RSA Private Key Gotten From {path}"; #inaccessible + + return key; + +def opt_mine_block(path = None): + + global rsa_private_key; + + if (path): + rsa_private_key = get_private_key_from_file(path); + if not (rsa_private_key): + return None; + + if rsa_private_key is None: + msg_array[0] = "You must load or generate an RSA keypair first."; + return; + + try: + response = requests.get(f"{WIKIDECK_URL}/blocks"); + if response.status_code != 200: + msg_array[0] = f"Failed to fetch block to mine: {response.status_code}"; + return; + + block_to_mine = Block(data = response.json()); + block_to_mine.mine(rsa_private_key); + + response = requests.post(f"{WIKIDECK_URL}/blocks", json=block_to_mine.as_dict()); + if response.status_code == 200: + msg_array[0] = "Block successfully mined and submitted"; + else: + msg_array[0] = f"Failed to submit mined block: {response.status_code}"; + + except Exception as e: + msg_array[0] = f"Error during mining: {str(e)}"; + +def opt_generate_transaction(path = None, recv = None): + global rsa_private_key; + + if (path): + rsa_private_key = get_private_key_from_file(path); + if not (rsa_private_key): + return None; + + + if rsa_private_key is None: + msg_array[0] = "You must load or generate an RSA keypair first."; + return; + + if not (recv): + print("\nPaste the recipient's RSA Public Key (PEM format). End with an Empty Line (Press enter twice): "); + lines = []; + while True: + line = input(); + if not line.strip(): + break; + lines.append(line); + + receiver_pem = "\n".join(lines); + + try: + receiver_key = serialization.load_pem_public_key(receiver_pem.encode("utf-8")); + except Exception: + msg_array[0] = "Invalid public key format"; + return; + + else: + if not os.path.isfile(recv): + msg_array[0] = f"Reciever public key file not found {recv}"; + return None; + with open(recv, "rb") as recv_file: + receiver_key = serialization.load_pem_public_key(recv_file.read()); + + + + + try: + + cards = fetch_cards_json(rsa_private_key); + + if not cards: + msg_array[0] = "You have no cards to send"; + return; + + print("\nYour cards:"); + + for i, card in enumerate(cards): + print(f"{i+1}. Card ID: {card['cardId']}, Page ID: {card['pageId']}"); + + + try: + index: int = int(input("Select a card number to send: ").strip()) - 1; + + if not (0 <= index < len(cards)): + raise ValueError; + except ValueError: + msg_array[0] = "Invalid card selection"; + return; + + selected_card = cards[index]; + + tx = Transaction( + cardId = selected_card['cardId'], + sender = rsa_private_key.public_key(), + receiver=receiver_key, + authorPrivateKey=rsa_private_key + ); + + response = requests.post(f"{WIKIDECK_URL}/transactions", json=tx.as_dict()); + + if response.status_code != 200: + msg_array[0] = f"Transaction failed: {response.status_code}"; + else: + msg_array[0] = "Transaction submitted successfully"; + + except Exception as e: + msg_array[0] = f"Error creating transaction: {str(e)}"; + + +def opt_view_deck(): + + global rsa_private_key; + if rsa_private_key is None: + msg_array[0] = "You must load or generate an RSA keypair first."; + return; + + try: + cards = fetch_cards_json(rsa_private_key); + + + if len(cards) <= 0: + msg_array[0] = "You have no cards to view"; + return; + + for i, card in enumerate(cards): + print(f"{i+1}. Card ID: {card['cardId']}, Page ID: {card['pageId']}"); + + except Exception as e: + msg_array[0] = f"Error fetching cards: {str(e)}"; + + +def fetch_cards_json(rsa): + pem = rsa.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode("utf-8"); + + response = requests.get(f"{WIKIDECK_URL}/cards", params={"publicKey": pem}); + + if response.status_code != 200: + msg_array[0] = f"Failed to fetch cards: {response.status_code}"; + return; + + cards = response.json(); + return cards; + + +def clear_console(): + os.system('cls' if os.name == 'nt' else 'clear'); + +if __name__ == "__main__": + main(); diff --git a/wikideck_cli.py b/wikideck_cli.py new file mode 100755 index 0000000..ca7aa69 --- /dev/null +++ b/wikideck_cli.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 + +import argparse +import glob +import os +from cli import opt_generate_keypair +from cli import opt_save_private_key +from cli import opt_load_private_key +from cli import opt_save_public_key +from cli import opt_mine_block +from cli import opt_generate_transaction +from cli import opt_view_deck +from cli import msg_array +from cli import get_private_key_from_file +from cli import fetch_cards_json + +DEFAULT_FILENAME: str = "privatekey"; + +def resolve_path(path: str | None) -> str | None: + if not path or path.strip() == "": + return None; + + path = path.strip(); + + if not path.endswith(".pem"): + path += ".pem"; + + if os.path.exists(path) and os.path.isfile(path): + return path; + + return None; + + +def resolve_path_for_key_creation(filename:str | None) -> str: + default_name = "privatekey.pem"; + + if not filename or filename.strip() == "": + return default_name; + + filename = filename.strip(); + + if os.path.isdir(filename): + return os.path.join(filename, default_name); + + if not filename.endswith(".pem"): + filename += ".pem"; + + dir_path = os.path.dirname(filename); + if dir_path and not os.path.exists(dir_path): + os.makedirs(dir_path); + + return filename; + +def get_first_pem_file(): + pem_files = glob.glob("*.pem"); + + if pem_files: + first_pem = min(pem_files, key=os.path.getmtime); + return first_pem; + else: + return None; + +def handle_createkey(args): + rsa_key = opt_generate_keypair(); + filepath = resolve_path_for_key_creation(args.output); + opt_save_private_key(rsa_key, filepath); + print(msg_array[0]); + public_path = f"pub_{filepath}" + opt_save_public_key(rsa_key, public_path); + +def handle_mine(args): + file = resolve_path(args.use); + if not file: + file = get_first_pem_file(); + if not file: + msg_array[0] = "No valid private key found"; + return; + + opt_mine_block(file); + + +def handle_tx(args): + + file = resolve_path(args.use); + if not file: + file = get_first_pem_file(); + if not file: + msg_array[0] = "No valid private key found"; + return; + + recv = resolve_path(args.receiver); + if args.receiver and not recv: + msg_array[0] = "No valid public key found (receiver)"; + return; + + opt_generate_transaction(file, recv); + + +def handle_deck(args): + file = resolve_path(args.use); + if not file: + file = get_first_pem_file(); + if not file: + msg_array[0] = "No valid private key found"; + return; + + rsa = get_private_key_from_file(file); + if rsa is None: + return; + + cards = fetch_cards_json(rsa); + + if len(cards) < 1: + msg_array[0] = "You have no cards to view"; + return; + + for i, card in enumerate(cards): + print(f"{i+1}. Card ID: {card['cardId']}, Page ID: {card['pageId']}"); + + +def build_parser(): + parser = argparse.ArgumentParser(description="Wikideck CLI"); + subparsers = parser.add_subparsers(dest="command", required=True); + + #createkey + p_create = subparsers.add_parser("createkey",aliases=["ck"], help="Create an RSA keypair and save it to the current directory"); + p_create.add_argument("-O", "--output", help="Optional custom filename (.pem)"); + p_create.set_defaults(func=handle_createkey); + + #mine + p_mine = subparsers.add_parser("mine", help="Mine a block, using the first ./*.pem by default."); + p_mine.add_argument("-u", "--use", help="Use specified keypair (path to .pem file)"); + p_mine.set_defaults(func=handle_mine); + + #transaction + p_transaction = subparsers.add_parser("transaction", aliases=["tx"], help="Generate a transaction, using the first ./*.pem by default."); + p_transaction.add_argument("-u", "--use", help="Use specified keypair (path to .pem file)"); + p_transaction.add_argument("-to", "--receiver", help="Send to specified public key (path to .pem file)"); + p_transaction.set_defaults(func=handle_tx); + + #deck + p_deck = subparsers.add_parser("deck", help="Show all cards tied to the private key, using the first ./*.pem by default."); + p_deck.add_argument("-u", "--use", help="Use specified keypair (path to .pem file)"); + p_deck.set_defaults(func=handle_deck); + + return parser; + +def main(): + parser = build_parser(); + args = parser.parse_args(); + args.func(args); + print(msg_array[0]); + +if __name__ == "__main__": + main();