Skip to content

Commit

Permalink
fix: removes the secret key requirement from the server
Browse files Browse the repository at this point in the history
  • Loading branch information
tuddman committed Apr 26, 2024
1 parent 98e7776 commit 8a473bf
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 70 deletions.
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -41,28 +34,44 @@ 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.

Example using curl:

curl -X POST http://0.0.0.0:5558/upload
-F "file=@path/to/your/message_bundle.aes"
-H "X-HMAC: <HMAC_VALUE_OF_FILE_SIGNED_WITH_SECRET_KEY>"

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: <HMAC_VALUE_OF_FILE_SIGNED_WITH_SECRET_KEY>"
-H "X-HMAC: <HMAC_VALUE_OF_FILE_SIGNED_WITH_SIGNING_KEY>"
-H "X-SIGNING-KEY: <KEY_THAT_SIGNED_THE_BUNDLE>"
--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

Expand Down
23 changes: 15 additions & 8 deletions examples/fetch-bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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)
30 changes: 15 additions & 15 deletions examples/upload-bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand All @@ -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
Expand All @@ -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)


80 changes: 45 additions & 35 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@ type HmacSha256 = Hmac<Sha256>;
// Define a state to hold the mapping of UUIDs to file names.
struct AppState {
file_map: Mutex<HashMap<Uuid, String>>,
secret_key: String,
}

async fn upload_file(
req: HttpRequest,
_req: HttpRequest,
mut payload: web::Payload,
data: web::Data<AppState>,
) -> 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();
Expand All @@ -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.
Expand All @@ -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);
Expand All @@ -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()) {
Expand All @@ -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());
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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 || {
Expand All @@ -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::<Sha256>::new_from_slice(secret_key).expect("HMAC can take key of any size");
Hmac::<Sha256>::new_from_slice(signing_key).expect("HMAC can take key of any size");
mac.update(data);
encode(mac.finalize().into_bytes())
}
Expand All @@ -188,22 +197,21 @@ 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());
}

#[actix_web::test]
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()),
});

Expand All @@ -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")
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 8a473bf

Please sign in to comment.