diff --git a/README.md b/README.md index 3aa1c4c..26eff89 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,7 @@ Ensure you have the following installed: - Rust and Cargo. You can install them both from [https://rustup.rs](https://rustup.rs) - To run the example uploader, ensure python is correctly installed on your system. -Set the `SECRET_KEY` environment variable. - export SECRET_KEY=super-long-super-secret-unique-key-goes-here - -Set the `AES_KEY` environment variable. - - export AES_KEY=only-authorized-installations-should-share-this-key - ### Installing @@ -41,9 +34,14 @@ The server will start running on http://0.0.0.0:5558. ## Usage +Set the `AES_KEY` and `SIGNING_KEY` environment variables. + + export AES_KEY=only-authorized-installations-should-share-this-key + export SIGNING_KEY=used-to-sign-the-message-bundle + ### Uploading a File -To upload a file, send a POST request to http://0.0.0.1:5558/upload with the file data in the request body. The upload request must include an `X-HMAC` header using `SHA256` as the hashing algorithm. +To upload a file, send a POST request to http://0.0.0.1:5558/upload with the file data in the request body. The server will return a unique ID for the uploaded file. @@ -51,18 +49,29 @@ Example using curl: curl -X POST http://0.0.0.0:5558/upload -F "file=@path/to/your/message_bundle.aes" - -H "X-HMAC: " -Retrieving a File +### Retrieving a File -To retrieve an uploaded file, send a GET request to http://0.0.0.0:5558/files/{id}, where {id} is the unique ID returned by the server during the upload. +To retrieve an uploaded file, you must possess the `HMAC_SIGNATURE` and `SIGNING_KEY` of the message_bundle. + +The request must include + +- an `X-HMAC` header using `SHA256` as the hashing algorithm. +- an `X-SIGNING-KEY` header set to the `SIGNING_KEY` + + +Send a GET request to http://0.0.0.0:5558/files/{id}, where {id} is the unique ID returned by the server during the upload. Example using curl: curl http://0.0.0.0:5558/files/{id} - -H "X-HMAC: " + -H "X-HMAC: " + -H "X-SIGNING-KEY: " --output retrieved_file.aes +### Decrypting a File + +To decrypt the downloaded file, you must possess the `AES_KEY` that was used to encrypt the message_bundle. ### Example Reference Client diff --git a/examples/fetch-bundle.py b/examples/fetch-bundle.py index d7c8325..2cfd4c4 100644 --- a/examples/fetch-bundle.py +++ b/examples/fetch-bundle.py @@ -2,20 +2,21 @@ import requests import os -def decrypt(key, source): +def decrypt(source, key): parts = source.rsplit(".aes", 1) output = parts[0] if len(parts) > 1 else source key_str = key.decode('utf-8') pyAesCrypt.decryptFile(source, output, key_str) return -def download_messages_bundle(bundle_id, hmac_value, aes_key): +def download_messages_bundle(bundle_id, hmac_value, signing_key, aes_key): """ - Downloads a file from the specified endpoint using a GET request with an X-HMAC header. + Downloads a file from the specified endpoint using a GET request with X-HMAC & X-SIGNING-KEY headers. Parameters: - bundle_id (str): The unique identifier for the file. - hmac_value (str): The HMAC signature value of the file with `bundle_id`. + - signing_key (str): The key used to sign the bundle. - aes_key (str): The AES key used to decrypt the downloaded file. """ @@ -25,7 +26,7 @@ def download_messages_bundle(bundle_id, hmac_value, aes_key): # Send the GET request with the X-HMAC header hmac_value_str = hmac_value.decode('utf-8') - headers = {'X-HMAC': hmac_value_str} + headers = {'X-HMAC': hmac_value_str, 'X-SIGNING-KEY': signing_key} response = requests.get(url, headers=headers) if response.status_code == 200: @@ -35,7 +36,7 @@ def download_messages_bundle(bundle_id, hmac_value, aes_key): with open(file_name, 'wb') as file: file.write(response.content) - decrypt(aes_key, file_name) + decrypt(file_name, aes_key) print(f"Successfully decrypted {file_name}") else: @@ -54,11 +55,17 @@ def download_messages_bundle(bundle_id, hmac_value, aes_key): if not hmac_value: print("HMAC_VALUE environment variable is not set.") exit(1) - + + # Ensure the signing key is not empty + signing_key = os.environ.get("SIGNING_KEY", "").encode() + if not signing_key: + print("SIGNING_KEY environment variable is not set.") + exit(1) + # Ensure the aes key is not empty aes_key = os.environ.get("AES_KEY", "").encode() if not aes_key: print("AES_KEY environment variable is not set.") exit(1) - - download_messages_bundle(bundle_id, hmac_value, aes_key) + + download_messages_bundle(bundle_id, hmac_value, signing_key, aes_key) diff --git a/examples/upload-bundle.py b/examples/upload-bundle.py index 2b97436..0f4f49b 100644 --- a/examples/upload-bundle.py +++ b/examples/upload-bundle.py @@ -6,20 +6,26 @@ file_path = "test_bundle.jsonl" -def upload_message_bundle(file_path, secret_key): +def encrypt(source, key): + key_str = key.decode('utf-8') + output = source + ".aes" + pyAesCrypt.encryptFile(source, output, key_str) + return output + +def upload_message_bundle(file_path, signing_key): """ Uploads a file to the specified endpoint. The file should be encrypted. Parameters: - file_path (str): The path to the file to upload. - - secret_key (str): The secret key to use to sign the HMAC signature with. + - signing_key (str): The key used to sign the HMAC. """ # The request payload consisting of a message history bundle with open(file_path, 'rb') as file: file_content = file.read() # Compute the HMAC - hmac_instance = hmac.new(secret_key, file_content, hashlib.sha256) + hmac_instance = hmac.new(signing_key, file_content, hashlib.sha256) hmac_hex = hmac_instance.hexdigest() print(f"HMAC: {hmac_hex}") @@ -30,17 +36,11 @@ def upload_message_bundle(file_path, secret_key): print(f"Response Status Code: {response.status_code}") print(f"Response Body: {response.text}") -def encrypt(key, source): - key_str = key.decode('utf-8') - output = source + ".aes" - pyAesCrypt.encryptFile(source, output, key_str) - return output - if __name__ == "__main__": - # Ensure the secret key is not empty - secret_key = os.environ.get("SECRET_KEY", "").encode() - if not secret_key: - print("SECRET_KEY environment variable is not set.") + # Ensure the signing key is not empty + signing_key = os.environ.get("SIGNING_KEY", "").encode() + if not signing_key: + print("SIGNING_KEY environment variable is not set.") exit(1) # Ensure the aes key is not empty @@ -49,8 +49,8 @@ def encrypt(key, source): print("AES_KEY environment variable is not set.") exit(1) - encrypted_file = encrypt(aes_key, file_path) + encrypted_file = encrypt(file_path, aes_key) - upload_message_bundle(encrypted_file, secret_key) + upload_message_bundle(encrypted_file, signing_key) diff --git a/src/main.rs b/src/main.rs index 7dc4a9e..b096b42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,18 +16,17 @@ type HmacSha256 = Hmac; // Define a state to hold the mapping of UUIDs to file names. struct AppState { file_map: Mutex>, - secret_key: String, } async fn upload_file( - req: HttpRequest, + _req: HttpRequest, mut payload: web::Payload, data: web::Data, ) -> impl Responder { - let hmac_header = match req.headers().get("X-HMAC") { - Some(value) => value.to_str().unwrap_or_default(), - None => return HttpResponse::BadRequest().body("Missing X-HMAC header"), - }; + // let hmac_header = match req.headers().get("X-HMAC") { + // Some(value) => value.to_str().unwrap_or_default(), + // None => return HttpResponse::BadRequest().body("Missing X-HMAC header"), + // }; // Create a new UUID for the file. let file_id = Uuid::new_v4(); @@ -41,7 +40,11 @@ async fn upload_file( #[cfg(test)] { use tempfile::NamedTempFile; - file_name = NamedTempFile::new().unwrap().path().to_string_lossy().to_string(); + file_name = NamedTempFile::new() + .unwrap() + .path() + .to_string_lossy() + .to_string(); } // Try to create the file. @@ -58,19 +61,21 @@ async fn upload_file( } } - let mut buffer = Vec::new(); - if let Ok(mut f) = File::open(file_name.clone()) { - let _ = f.read_to_end(&mut buffer); - } else { - return HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR); - } - - // Verify HMAC - let secret_key = data.secret_key.as_bytes(); - if let Err(response) = verify_hmac(hmac_header, &buffer, secret_key) { - let _ = std::fs::remove_file(file_name); - return response; - } + // Commented below, as we aren't verifying the uploaded file. Anything can be uploaded. + + // let mut buffer = Vec::new(); + // if let Ok(mut f) = File::open(file_name.clone()) { + // let _ = f.read_to_end(&mut buffer); + // } else { + // return HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR); + // } + // + // // Verify HMAC + // let secret_key = data.secret_key.as_bytes(); + // if let Err(response) = verify_hmac(hmac_header, &buffer, secret_key) { + // let _ = std::fs::remove_file(file_name); + // return response; + // } // Insert the file ID and name into the state map. data.file_map.lock().unwrap().insert(file_id, file_name); @@ -89,6 +94,12 @@ async fn get_file( None => return HttpResponse::BadRequest().body("Missing X-HMAC header"), }; + // Extract the X-SIGNING-KEY header + let signing_key = match req.headers().get("X-SIGNING-KEY") { + Some(value) => value.to_str().unwrap_or_default(), + None => return HttpResponse::BadRequest().body("Missing X-SIGNING-KEY header"), + }; + let file_map = data.file_map.lock().unwrap(); if let Some(file_name) = file_map.get(&path.into_inner()) { @@ -103,7 +114,7 @@ async fn get_file( } // Compute the HMAC for the file content - let mut mac = HmacSha256::new_from_slice(data.secret_key.as_bytes()) + let mut mac = HmacSha256::new_from_slice(signing_key.as_bytes()) .expect("HMAC can take key of any size"); mac.update(&buffer); let result_hmac = encode(mac.finalize().into_bytes()); @@ -125,7 +136,7 @@ async fn get_file( fn verify_hmac( hmac_header: &str, file_bytes: &[u8], - secret_key: &[u8], + signing_key: &[u8], ) -> Result<(), HttpResponse> { // Decode the hex HMAC let received_hmac = match decode(hmac_header) { @@ -134,7 +145,7 @@ fn verify_hmac( }; // Create an instance of the HMAC-SHA256 - let mut mac = HmacSha256::new_from_slice(secret_key).expect("Insufficient HMAC key size"); + let mut mac = HmacSha256::new_from_slice(signing_key).expect("Insufficient HMAC key size"); // Input the data to the HMAC instance mac.update(file_bytes); @@ -149,11 +160,9 @@ fn verify_hmac( #[actix_web::main] async fn main() -> std::io::Result<()> { std::fs::create_dir_all("uploads").unwrap(); - let secret_key = std::env::var("SECRET_KEY").unwrap(); let app_state = web::Data::new(AppState { file_map: Mutex::new(HashMap::new()), - secret_key, }); HttpServer::new(move || { @@ -173,12 +182,12 @@ mod tests { use actix_web::test; use tempfile::tempdir; - const SECRET_KEY: &[u8] = b"TEST_SECRET_KEY"; + const SIGNING_KEY: &[u8] = b"TEST_SECRET_KEY"; // Test helper function to create a HMAC signature - fn create_hmac_signature(secret_key: &[u8], data: &[u8]) -> String { + fn create_hmac_signature(signing_key: &[u8], data: &[u8]) -> String { let mut mac = - Hmac::::new_from_slice(secret_key).expect("HMAC can take key of any size"); + Hmac::::new_from_slice(signing_key).expect("HMAC can take key of any size"); mac.update(data); encode(mac.finalize().into_bytes()) } @@ -188,14 +197,14 @@ mod tests { async fn test_hmac_verification() { // Test valid HMAC passes as Ok(()) let correct_payload = b"correct payload"; - let correct_hmac = create_hmac_signature(SECRET_KEY, correct_payload); - let verify_correct = verify_hmac(&correct_hmac, correct_payload, SECRET_KEY); + let correct_hmac = create_hmac_signature(SIGNING_KEY, correct_payload); + let verify_correct = verify_hmac(&correct_hmac, correct_payload, SIGNING_KEY); assert!(verify_correct.is_ok(), "Should succeed with correct HMAC"); // Test invvalid HMAC returns an HttpResponse()) let incorrect_payload = b"incorrect payload"; - let verify_incorrect = verify_hmac(&correct_hmac, incorrect_payload, SECRET_KEY); + let verify_incorrect = verify_hmac(&correct_hmac, incorrect_payload, SIGNING_KEY); assert!(verify_incorrect.is_err()); } @@ -203,7 +212,6 @@ mod tests { async fn test_upload_file() { // Set up application state let data = web::Data::new(AppState { - secret_key: std::str::from_utf8(SECRET_KEY).unwrap().to_string(), file_map: Mutex::new(HashMap::new()), }); @@ -226,7 +234,7 @@ mod tests { ); let correct_payload = b"correct payload"; - let correct_hmac = create_hmac_signature(SECRET_KEY, correct_payload); + let correct_hmac = create_hmac_signature(SIGNING_KEY, correct_payload); let req = test::TestRequest::post() .uri("/upload") @@ -249,12 +257,13 @@ mod tests { async fn test_get_file() { // Set up application state let data = web::Data::new(AppState { - secret_key: std::str::from_utf8(SECRET_KEY).unwrap().to_string(), file_map: Mutex::new(HashMap::new()), }); + // and the secret key + let signing_key = std::str::from_utf8(SIGNING_KEY).unwrap().to_string(); let file_contents = b"this too shall pass!"; // Simulated file contents - let correct_hmac = create_hmac_signature(SECRET_KEY, file_contents); + let correct_hmac = create_hmac_signature(SIGNING_KEY, file_contents); let incorrect_hmac = String::from("none shall pass"); let temp_dir = tempdir().unwrap(); @@ -284,6 +293,7 @@ mod tests { let req = test::TestRequest::get() .uri(&format!("/files/{file_id}")) .insert_header(("X-HMAC", correct_hmac)) + .insert_header(("X-SIGNING-KEY", signing_key)) .to_request(); let resp = test::call_service(&app, req).await;